From 1857e6b293c10a6740ebc07cc0d2bb73f16a3638 Mon Sep 17 00:00:00 2001
From: Anatoly Ostrovsky
Date: Thu, 13 Nov 2025 01:14:21 +0200
Subject: [PATCH 1/8] Sanitize ngAttrSrcset images
---
src/core/compile/compile.js | 15 ++++++++++++---
1 file changed, 12 insertions(+), 3 deletions(-)
diff --git a/src/core/compile/compile.js b/src/core/compile/compile.js
index 1264cce70..bf2ac2846 100644
--- a/src/core/compile/compile.js
+++ b/src/core/compile/compile.js
@@ -2767,14 +2767,23 @@ export class CompileProvider {
attr.$$element.classList.value,
);
} else {
- attr.$set(name, newValue);
+ attr.$set(
+ name,
+ name === "srcset"
+ ? $sce.getTrustedMediaUrl(newValue)
+ : newValue,
+ );
}
});
});
if (interpolateFn.expressions.length == 0) {
- // if there is nothing to watch, its a constant
- attr.$set(name, newValue);
+ attr.$set(
+ name,
+ name === "srcset"
+ ? $sce.getTrustedMediaUrl(newValue)
+ : newValue,
+ );
}
},
};
From 912c3b4a20ec085a6fd148e0d3b666dbc8ef5fb3 Mon Sep 17 00:00:00 2001
From: Anatoly Ostrovsky
Date: Thu, 13 Nov 2025 21:10:16 +0200
Subject: [PATCH 2/8] Fix for impure binary expression. Types and test
imrovements
---
@types/core/scope/interface.d.ts | 4 +-
@types/core/scope/scope.d.ts | 4 +-
@types/namespace.d.ts | 8 +-
@types/shared/utils.d.ts | 13 +-
src/animations/animate.js | 17 +--
src/animations/animate.spec.js | 6 +-
src/animations/runner/animate-runner.js | 2 +-
src/core/interpolate/interpolate.spec.js | 8 +
src/core/scope/interface.ts | 4 +-
src/core/scope/scope.js | 37 ++++-
src/core/scope/scope.spec.js | 2 +-
src/directive/http/http.js | 1 +
src/namespace.ts | 8 +-
src/services/sse/sse.js | 2 +-
src/shared/noderef.js | 21 +--
src/shared/noderef.spec.js | 187 +++++++++++++++++++++++
src/shared/shared.html | 1 +
src/shared/utils.js | 35 ++++-
src/shared/utils.spec.js | 70 ++++++++-
19 files changed, 379 insertions(+), 51 deletions(-)
create mode 100644 src/shared/noderef.spec.js
diff --git a/@types/core/scope/interface.d.ts b/@types/core/scope/interface.d.ts
index b46815bf4..56f7e6695 100644
--- a/@types/core/scope/interface.d.ts
+++ b/@types/core/scope/interface.d.ts
@@ -5,10 +5,10 @@ export interface AsyncQueueTask {
fn: (...args: any[]) => any;
locals: Record;
}
-export type ListenerFunction = (newValue: any, originalTarget: object) => void;
+export type ListenerFn = (newValue?: any, originalTarget?: object) => void;
export interface Listener {
originalTarget: object;
- listenerFn: ListenerFunction;
+ listenerFn: ListenerFn;
watchFn: CompiledExpression;
id: number;
scopeId: number;
diff --git a/@types/core/scope/scope.d.ts b/@types/core/scope/scope.d.ts
index 47d3feb5a..1f80bd38d 100644
--- a/@types/core/scope/scope.d.ts
+++ b/@types/core/scope/scope.d.ts
@@ -128,12 +128,12 @@ export class Scope {
* function is invoked when changes to that property are detected.
*
* @param {string} watchProp - An expression to be watched in the context of this model.
- * @param {import('./interface.ts').ListenerFunction} [listenerFn] - A function to execute when changes are detected on watched context.
+ * @param {ng.ListenerFn} [listenerFn] - A function to execute when changes are detected on watched context.
* @param {boolean} [lazy] - A flag to indicate if the listener should be invoked immediately. Defaults to false.
*/
$watch(
watchProp: string,
- listenerFn?: import("./interface.ts").ListenerFunction,
+ listenerFn?: ng.ListenerFn,
lazy?: boolean,
): () => void;
$new(childInstance: any): any;
diff --git a/@types/namespace.d.ts b/@types/namespace.d.ts
index a4d0e21e4..1c26c3434 100644
--- a/@types/namespace.d.ts
+++ b/@types/namespace.d.ts
@@ -2,6 +2,10 @@ export { angular } from "./index.js";
import { Angular as TAngular } from "./angular.js";
import { Attributes as TAttributes } from "./core/compile/attributes.js";
import { Scope as TScope } from "./core/scope/scope.js";
+import {
+ ListenerFn as TListenerFn,
+ Listener as TListener,
+} from "./core/scope/interface.ts";
import { NgModule as TNgModule } from "./core/di/ng-module.js";
import { InjectorService as TInjectorService } from "./core/di/internal-injector.js";
import {
@@ -107,8 +111,10 @@ declare global {
type TemplateCacheService = Map;
type TemplateRequestService = TTemplateRequestService;
type ErrorHandlingConfig = TErrorHandlingConfig;
- type WindowService = Window;
+ type ListenerFn = TListenerFn;
+ type Listener = TListener;
type DocumentService = Document;
+ type WindowService = Window;
type WorkerConfig = TWorkerConfig;
type WorkerConnection = TWorkerConnection;
}
diff --git a/@types/shared/utils.d.ts b/@types/shared/utils.d.ts
index 3764dcee5..a93aa7b95 100644
--- a/@types/shared/utils.d.ts
+++ b/@types/shared/utils.d.ts
@@ -488,7 +488,18 @@ export function toDebugString(obj: any): any;
* The resulting string key is in 'type:hashKey' format.
*/
export function hashKey(obj: any): string;
-export function mergeClasses(a: any, b: any): any;
+/**
+ * Merges two class name values into a single space-separated string.
+ * Accepts strings, arrays of strings, or null/undefined values.
+ *
+ * @param {string | string[] | null | undefined} a - The first class name(s).
+ * @param {string | string[] | null | undefined} b - The second class name(s).
+ * @returns {string} A single string containing all class names separated by spaces.
+ */
+export function mergeClasses(
+ a: string | string[] | null | undefined,
+ b: string | string[] | null | undefined,
+): string;
/**
* Converts all accepted directives format into proper directive name.
* @param {string} name Name to normalize
diff --git a/src/animations/animate.js b/src/animations/animate.js
index b9b37b53d..981c71358 100644
--- a/src/animations/animate.js
+++ b/src/animations/animate.js
@@ -1,4 +1,10 @@
-import { isFunction, isObject, minErr, extend } from "../shared/utils.js";
+import {
+ isFunction,
+ isObject,
+ minErr,
+ extend,
+ mergeClasses,
+} from "../shared/utils.js";
import { removeElement, animatedomInsert } from "../shared/dom.js";
import { NG_ANIMATE_CLASSNAME } from "./shared.js";
@@ -14,15 +20,6 @@ import { NG_ANIMATE_CLASSNAME } from "./shared.js";
const $animateMinErr = minErr("$animate");
-function mergeClasses(a, b) {
- if (!a && !b) return "";
- if (!a) return b;
- if (!b) return a;
- if (Array.isArray(a)) a = a.join(" ");
- if (Array.isArray(b)) b = b.join(" ");
- return `${a} ${b}`;
-}
-
// if any other type of options value besides an Object value is
// passed into the $animate.method() animation then this helper code
// will be run which will ignore it. While this patch is not the
diff --git a/src/animations/animate.spec.js b/src/animations/animate.spec.js
index cb2b3e592..fb3306409 100644
--- a/src/animations/animate.spec.js
+++ b/src/animations/animate.spec.js
@@ -1,6 +1,6 @@
import { createElementFromHTML, dealoc } from "../shared/dom.js";
import { Angular } from "../angular.js";
-import { isObject } from "../shared/utils.js";
+import { isObject, mergeClasses } from "../shared/utils.js";
import { isFunction, wait } from "../shared/utils.js";
import { createInjector } from "../core/di/injector.js";
@@ -496,4 +496,8 @@ describe("$animate", () => {
});
});
});
+
+ describe("mergeClasses", () => {
+ expect(mergeClasses);
+ });
});
diff --git a/src/animations/runner/animate-runner.js b/src/animations/runner/animate-runner.js
index 0ec701def..673bd41ce 100644
--- a/src/animations/runner/animate-runner.js
+++ b/src/animations/runner/animate-runner.js
@@ -2,7 +2,7 @@
* @fileoverview
* Frame-synchronized animation runner and scheduler.
* Provides async batching of animation callbacks using requestAnimationFrame.
- * In AngularJS, this user to be implemented as `$$AnimateRunner`
+ * In AngularJS, this used to be implemented as `$$AnimateRunner`
*/
/**
diff --git a/src/core/interpolate/interpolate.spec.js b/src/core/interpolate/interpolate.spec.js
index 51b2c2cc1..95bdec688 100644
--- a/src/core/interpolate/interpolate.spec.js
+++ b/src/core/interpolate/interpolate.spec.js
@@ -29,6 +29,14 @@ describe("$interpolate", () => {
expect(interp({})).toEqual("why u no }}work{{");
});
+ it("evaluates binary expressions", function () {
+ let interp = $interpolate("{{a + b}}");
+ expect(interp({ a: 11, b: 22 })).toEqual("33");
+
+ interp = $interpolate("{{a + b + c}}");
+ expect(interp({ a: 11, b: 22, c: 33 })).toEqual("66");
+ });
+
it("evaluates many expressions", function () {
const interp = $interpolate("First {{anAttr}}, then {{anotherAttr}}!");
expect(interp({ anAttr: "42", anotherAttr: "43" })).toEqual(
diff --git a/src/core/scope/interface.ts b/src/core/scope/interface.ts
index 360eaeeab..6626df9b8 100644
--- a/src/core/scope/interface.ts
+++ b/src/core/scope/interface.ts
@@ -7,11 +7,11 @@ export interface AsyncQueueTask {
locals: Record;
}
-export type ListenerFunction = (newValue: any, originalTarget: object) => void;
+export type ListenerFn = (newValue?: any, originalTarget?: object) => void;
export interface Listener {
originalTarget: object;
- listenerFn: ListenerFunction;
+ listenerFn: ListenerFn;
watchFn: CompiledExpression;
id: number; // Deregistration id
scopeId: number; // The scope id that created the Listener
diff --git a/src/core/scope/scope.js b/src/core/scope/scope.js
index c6c1215e4..562b04de1 100644
--- a/src/core/scope/scope.js
+++ b/src/core/scope/scope.js
@@ -616,7 +616,7 @@ export class Scope {
* function is invoked when changes to that property are detected.
*
* @param {string} watchProp - An expression to be watched in the context of this model.
- * @param {import('./interface.ts').ListenerFunction} [listenerFn] - A function to execute when changes are detected on watched context.
+ * @param {ng.ListenerFn} [listenerFn] - A function to execute when changes are detected on watched context.
* @param {boolean} [lazy] - A flag to indicate if the listener should be invoked immediately. Defaults to false.
*/
$watch(watchProp, listenerFn, lazy = false) {
@@ -638,7 +638,7 @@ export class Scope {
return () => {};
}
- /** @type {import('./interface.ts').Listener} */
+ /** @type {ng.Listener} */
const listener = {
originalTarget: this.$target,
listenerFn: listenerFn,
@@ -696,13 +696,34 @@ export class Scope {
}
// 6
case ASTType.BinaryExpression: {
- let expr = get.decoratedNode.body[0].expression.toWatch[0];
- key = expr.property ? expr.property.name : expr.name;
- if (!key) {
- throw new Error("Unable to determine key");
+ if (get.decoratedNode.body[0].expression.isPure) {
+ let expr = get.decoratedNode.body[0].expression.toWatch[0];
+ key = expr.property ? expr.property.name : expr.name;
+ if (!key) {
+ throw new Error("Unable to determine key");
+ }
+ listener.property.push(key);
+ break;
+ } else {
+ let keys = [];
+ get.decoratedNode.body[0].expression.toWatch.forEach((x) => {
+ const k = x.property ? x.property.name : x.name;
+ if (!k) {
+ throw new Error("Unable to determine key");
+ }
+ keys.push(k);
+ });
+ keys.forEach((key) => {
+ this.#registerKey(key, listener);
+ this.#scheduleListener([listener]);
+ });
+
+ return () => {
+ keys.forEach((key) => {
+ this.#deregisterKey(key, listener.id);
+ });
+ };
}
- listener.property.push(key);
- break;
}
// 7
case ASTType.UnaryExpression: {
diff --git a/src/core/scope/scope.spec.js b/src/core/scope/scope.spec.js
index 3aa9ff7f1..cc9663b52 100644
--- a/src/core/scope/scope.spec.js
+++ b/src/core/scope/scope.spec.js
@@ -107,7 +107,7 @@ describe("Scope", () => {
});
describe("$nonscope", () => {
- it("should ignore objects with $nonscope property", () => {
+ it("should ignore objects with $nonscope propercoty", () => {
const res = createScope({ $nonscope: true });
expect(res.$id).toBeUndefined();
});
diff --git a/src/directive/http/http.js b/src/directive/http/http.js
index d5ccbea40..7ca44b570 100644
--- a/src/directive/http/http.js
+++ b/src/directive/http/http.js
@@ -77,6 +77,7 @@ export function createHttpDirective(method, attrName) {
* @param {ng.ParseService} $parse
* @param {ng.StateService} $state
* @param {ng.SseService} $sse
+ * @param {*} $animate
* @returns {ng.Directive}
*/
return function ($http, $compile, $log, $parse, $state, $sse, $animate) {
diff --git a/src/namespace.ts b/src/namespace.ts
index 3e8a5b3b4..dd4f76e00 100644
--- a/src/namespace.ts
+++ b/src/namespace.ts
@@ -5,6 +5,10 @@ export { angular } from "./index.js";
import { Angular as TAngular } from "./angular.js";
import { Attributes as TAttributes } from "./core/compile/attributes.js";
import { Scope as TScope } from "./core/scope/scope.js";
+import {
+ ListenerFn as TListenerFn,
+ Listener as TListener,
+} from "./core/scope/interface.ts";
import { NgModule as TNgModule } from "./core/di/ng-module.js";
import { InjectorService as TInjectorService } from "./core/di/internal-injector.js";
@@ -130,8 +134,10 @@ declare global {
// Support types
export type ErrorHandlingConfig = TErrorHandlingConfig;
- export type WindowService = Window;
+ export type ListenerFn = TListenerFn;
+ export type Listener = TListener;
export type DocumentService = Document;
+ export type WindowService = Window;
export type WorkerConfig = TWorkerConfig;
export type WorkerConnection = TWorkerConnection;
}
diff --git a/src/services/sse/sse.js b/src/services/sse/sse.js
index 9c22c631a..c9ca0520b 100644
--- a/src/services/sse/sse.js
+++ b/src/services/sse/sse.js
@@ -134,7 +134,7 @@ export class SseProvider {
es.close();
},
connect() {
- if (closed == false) {
+ if (closed === false) {
close();
}
connect();
diff --git a/src/shared/noderef.js b/src/shared/noderef.js
index 06de2e36e..ac21e1734 100644
--- a/src/shared/noderef.js
+++ b/src/shared/noderef.js
@@ -67,8 +67,8 @@ export class NodeRef {
}
// Handle array of elements
- else if (element instanceof Array) {
- if (element.length == 1) {
+ else if (Array.isArray(element)) {
+ if (element.length === 1) {
this.initial = element[0].cloneNode(true);
this.node = element[0];
} else {
@@ -129,19 +129,12 @@ export class NodeRef {
/** @returns {NodeList|Node[]} */
get nodelist() {
- assertArg(this.isList, "nodes");
- if (this._nodes.length === 0) {
- return this._nodes;
- }
- if (this._nodes[0].parentElement) {
+ if (this._nodes.length === 0) return [];
+ if (this._nodes[0].parentElement)
return this._nodes[0].parentElement.childNodes;
- } else {
- const fragment = document.createDocumentFragment();
- this._nodes.forEach((el) => {
- fragment.appendChild(el);
- });
- return fragment.childNodes;
- }
+ const fragment = document.createDocumentFragment();
+ this._nodes.forEach((el) => fragment.appendChild(el));
+ return fragment.childNodes;
}
/** @returns {Element | Node | ChildNode | NodeList | Node[]} */
diff --git a/src/shared/noderef.spec.js b/src/shared/noderef.spec.js
new file mode 100644
index 000000000..25745faf6
--- /dev/null
+++ b/src/shared/noderef.spec.js
@@ -0,0 +1,187 @@
+import { NodeRef } from "./noderef.js";
+
+describe("NodeRef", () => {
+ let div;
+ let span;
+ let textNode;
+
+ beforeEach(() => {
+ div = document.createElement("div");
+ div.textContent = "hello";
+ span = document.createElement("span");
+ span.textContent = "world";
+ textNode = document.createTextNode("plain text");
+ });
+
+ describe("constructor", () => {
+ it("wraps a single Element", () => {
+ const ref = new NodeRef(div);
+ expect(ref.node).toBe(div);
+ expect(ref.element).toBe(div);
+ expect(ref.size).toBe(1);
+ expect(ref.isList).toBeFalse();
+ expect(ref.isElement()).toBeTrue();
+ });
+
+ it("wraps a single Node", () => {
+ const ref = new NodeRef(textNode);
+ expect(ref.node).toBe(textNode);
+ expect(ref.isElement()).toBeFalse();
+ });
+
+ it("wraps an array of Nodes", () => {
+ const ref = new NodeRef([div, span]);
+ expect(ref.nodes).toEqual([div, span]);
+ expect(ref.isList).toBeTrue();
+ expect(ref.size).toBe(2);
+ });
+
+ it("wraps an array with one Node", () => {
+ const ref = new NodeRef([div]);
+ expect(ref.node).toBe(div);
+ expect(ref.isList).toBeFalse();
+ expect(ref.size).toBe(1);
+ });
+
+ it("wraps a NodeList", () => {
+ const parent = document.createElement("div");
+ parent.append(div, span);
+ const ref = new NodeRef(parent.childNodes);
+ expect(ref.nodes).toEqual([div, span]);
+ expect(ref.isList).toBeTrue();
+ });
+
+ it("wraps an HTML string", () => {
+ const html = "
hi
";
+ const ref = new NodeRef(html);
+ expect(ref.element.tagName.toLowerCase()).toBe("p");
+ expect(ref.element.textContent).toBe("hi");
+ });
+
+ it("handles HTML string with multiple nodes", () => {
+ const html = "
A
B";
+ const ref = new NodeRef(html);
+ expect(ref.getAny().outerHTML).toContain("div");
+ });
+
+ it("throws on invalid input", () => {
+ expect(() => new NodeRef(123)).toThrowError(
+ "Invalid element passed to NodeRef",
+ );
+ expect(() => new NodeRef(null)).toThrow();
+ expect(() => new NodeRef(undefined)).toThrow();
+ });
+ });
+
+ describe("getters and setters", () => {
+ it("sets and gets node", () => {
+ const ref = new NodeRef(div);
+ const newDiv = document.createElement("div");
+ ref.node = newDiv;
+ expect(ref.node).toBe(newDiv);
+ expect(ref.element).toBe(newDiv);
+ });
+
+ it("sets and gets nodes array", () => {
+ const ref = new NodeRef(div);
+ ref.nodes = [div, span];
+ expect(ref.nodes).toEqual([div, span]);
+ expect(ref.isList).toBeTrue();
+ });
+
+ it("returns correct element and node", () => {
+ const ref = new NodeRef(div);
+ expect(ref.element).toBe(div);
+ expect(ref.node).toBe(div);
+ });
+
+ it("returns size correctly", () => {
+ const ref1 = new NodeRef(div);
+ const ref2 = new NodeRef([div, span]);
+ expect(ref1.size).toBe(1);
+ expect(ref2.size).toBe(2);
+ });
+ });
+
+ describe("list and collection methods", () => {
+ it("getAny() returns first node of list", () => {
+ const ref = new NodeRef([div, span]);
+ expect(ref.getAny()).toBe(div);
+ });
+
+ it("getAll() returns all nodes", () => {
+ const ref = new NodeRef([div, span]);
+ expect(ref.getAll()).toEqual([div, span]);
+ });
+
+ it("collection() always returns array", () => {
+ const ref1 = new NodeRef(div);
+ const ref2 = new NodeRef([div, span]);
+ expect(ref1.collection()).toEqual([div]);
+ expect(ref2.collection()).toEqual([div, span]);
+ });
+
+ it("getIndex() and setIndex() work properly", () => {
+ const ref = new NodeRef([div, span]);
+ expect(ref.getIndex(1)).toBe(span);
+ const newNode = document.createElement("p");
+ ref.setIndex(1, newNode);
+ expect(ref.getIndex(1)).toBe(newNode);
+ });
+ });
+
+ describe("nodelist and clone", () => {
+ it("returns live nodelist if attached to DOM", () => {
+ const parent = document.createElement("div");
+ parent.append(div, span);
+ const ref = new NodeRef([div, span]);
+ const list = ref.nodelist;
+ expect(list[0]).toBe(div);
+ expect(list[1]).toBe(span);
+ });
+
+ it("returns fragment-based nodelist if detached", () => {
+ const ref = new NodeRef([div, span]);
+ const list = ref.nodelist;
+ expect(list.length).toBe(2);
+ expect(list[0].isEqualNode(div)).toBeTrue();
+ });
+
+ it("clone() creates deep copy", () => {
+ const ref = new NodeRef([div, span]);
+ const clone = ref.clone();
+ expect(clone.nodes[0].isEqualNode(div)).toBeTrue();
+ expect(clone.nodes[0]).not.toBe(div);
+ });
+
+ it("clone() works on single element", () => {
+ const ref = new NodeRef(div);
+ const clone = ref.clone();
+ expect(clone.node.isEqualNode(div)).toBeTrue();
+ expect(clone.node).not.toBe(div);
+ });
+ });
+
+ describe("edge cases", () => {
+ it("handles text node only", () => {
+ const ref = new NodeRef(textNode);
+ expect(ref.node.textContent).toBe("plain text");
+ expect(ref.isElement()).toBeFalse();
+ });
+
+ it("handles DocumentFragment", () => {
+ const frag = document.createDocumentFragment();
+ frag.append(div, span);
+ const ref = new NodeRef(frag.childNodes);
+ expect(ref.isList).toBeTrue();
+ expect(ref.nodes.length).toBe(2);
+ });
+
+ it("handles malformed HTML string gracefully", () => {
+ const html = "
Missing close";
+ const ref = new NodeRef(html);
+ expect(ref.node).toBeTruthy();
+ expect(ref.getAny().nodeType).toBe(Node.ELEMENT_NODE);
+ });
+ });
+});
diff --git a/src/shared/shared.html b/src/shared/shared.html
index ca51488d5..d3be5190f 100644
--- a/src/shared/shared.html
+++ b/src/shared/shared.html
@@ -14,6 +14,7 @@
+
diff --git a/src/shared/utils.js b/src/shared/utils.js
index df2768afd..316cd08c8 100644
--- a/src/shared/utils.js
+++ b/src/shared/utils.js
@@ -1106,13 +1106,38 @@ export function hashKey(obj) {
return `${objType}:${obj}`;
}
+/**
+ * Merges two class name values into a single space-separated string.
+ * Accepts strings, arrays of strings, or null/undefined values.
+ *
+ * @param {string | string[] | null | undefined} a - The first class name(s).
+ * @param {string | string[] | null | undefined} b - The second class name(s).
+ * @returns {string} A single string containing all class names separated by spaces.
+ */
export function mergeClasses(a, b) {
if (!a && !b) return "";
- if (!a) return b;
- if (!b) return a;
- if (Array.isArray(a)) a = a.join(" ");
- if (Array.isArray(b)) b = b.join(" ");
- return a + " " + b;
+ if (!a) return Array.isArray(b) ? b.join(" ").trim() : b;
+ if (!b) return Array.isArray(a) ? a.join(" ").trim() : a;
+ if (Array.isArray(a)) a = normalizeStringArray(a);
+ if (Array.isArray(b)) b = normalizeStringArray(b);
+ return `${a.trim()} ${b.trim()}`.trim();
+}
+
+/**
+ * Joins an array of strings into a single string, trimming each
+ * element and ignoring empty strings, null, and undefined
+ * @param {any[]} arr
+ * @returns {string}
+ */
+function normalizeStringArray(arr) {
+ const cleaned = [];
+ for (const item of arr) {
+ if (item) {
+ const trimmed = item.trim();
+ if (trimmed) cleaned.push(trimmed);
+ }
+ }
+ return cleaned.join(" ");
}
/**
diff --git a/src/shared/utils.spec.js b/src/shared/utils.spec.js
index 1d6c65361..4dff379c5 100644
--- a/src/shared/utils.spec.js
+++ b/src/shared/utils.spec.js
@@ -1,4 +1,4 @@
-import { hashKey, startsWith } from "./utils.js";
+import { hashKey, mergeClasses, startsWith } from "./utils.js";
describe("api", () => {
describe("hashKey()", () => {
@@ -175,4 +175,72 @@ describe("api", () => {
expect(startsWith("short", "shorter")).toBe(false);
});
});
+
+ describe("mergeClasses", () => {
+ it("should return empty string if both arguments are null or undefined", () => {
+ expect(mergeClasses(null, null)).toBe("");
+ expect(mergeClasses(undefined, undefined)).toBe("");
+ expect(mergeClasses(null, undefined)).toBe("");
+ });
+
+ it("should return first argument if second is null/undefined", () => {
+ expect(mergeClasses("btn", null)).toBe("btn");
+ expect(mergeClasses(["btn", "primary"], undefined)).toBe("btn primary");
+ });
+
+ it("should return second argument if first is null/undefined", () => {
+ expect(mergeClasses(null, "active")).toBe("active");
+ expect(mergeClasses(undefined, ["active", "large"])).toBe("active large");
+ });
+
+ it("should merge two strings with a space", () => {
+ expect(mergeClasses("btn", "active")).toBe("btn active");
+ });
+
+ it("should always trim merged strings", () => {
+ expect(mergeClasses("btn ", " active")).toBe("btn active");
+ });
+
+ it("should merge two arrays of strings", () => {
+ expect(mergeClasses(["btn", "primary"], ["active", "large"])).toBe(
+ "btn primary active large",
+ );
+ });
+
+ it("should merge string and array", () => {
+ expect(mergeClasses("btn", ["active", "large"])).toBe("btn active large");
+ expect(mergeClasses(["btn", "primary"], "active")).toBe(
+ "btn primary active",
+ );
+ });
+
+ it("should ignore empty strings, null, and undefined in arrays", () => {
+ expect(
+ mergeClasses(["btn", "", null, undefined], ["active", "", null]),
+ ).toBe("btn active");
+ });
+
+ it("should ignore empty strings, null, and undefined in arrays if second argument is empty", () => {
+ expect(mergeClasses(["btn", "", null, undefined], undefined)).toBe("btn");
+ });
+
+ it("should ignore empty strings, null, and undefined in arrays if first argument is empty", () => {
+ expect(mergeClasses(undefined, ["btn", "", null, undefined])).toBe("btn");
+ });
+
+ it("should handle single argument arrays correctly", () => {
+ expect(mergeClasses(["btn"], ["active"])).toBe("btn active");
+ });
+
+ it("should handle single argument arrays correctly", () => {
+ expect(mergeClasses(["btn ", " test"], [" active"])).toBe(
+ "btn test active",
+ );
+ });
+
+ it("should handle one argument as string and the other as empty array", () => {
+ expect(mergeClasses("btn", [])).toBe("btn");
+ expect(mergeClasses([], "active")).toBe("active");
+ });
+ });
});
From 89518acb03fffdc7667dc784c83ffa9d2842758e Mon Sep 17 00:00:00 2001
From: Anatoly Ostrovsky
Date: Fri, 14 Nov 2025 18:05:17 +0200
Subject: [PATCH 3/8] Test fixes
---
@types/core/sanitize/sanitize-uri.d.ts | 2 +-
src/animations/animate.spec.js | 6 +----
src/core/sanitize/sanitize-uri.js | 35 +++++++++++++++-----------
src/core/sanitize/sanitize-uri.spec.js | 2 +-
src/core/scope/scope.spec.js | 6 ++---
src/router/state/state.spec.js | 2 +-
6 files changed, 27 insertions(+), 26 deletions(-)
diff --git a/@types/core/sanitize/sanitize-uri.d.ts b/@types/core/sanitize/sanitize-uri.d.ts
index 17d3229e3..5f51d5156 100644
--- a/@types/core/sanitize/sanitize-uri.d.ts
+++ b/@types/core/sanitize/sanitize-uri.d.ts
@@ -35,6 +35,6 @@ export class SanitizeUriProvider implements ServiceProvider {
/**
* @returns {import("./interface").SanitizerFn}
*/
- $get(): any;
+ $get: (string | (($window: ng.WindowService) => any))[];
}
export type ServiceProvider = import("../../interface.ts").ServiceProvider;
diff --git a/src/animations/animate.spec.js b/src/animations/animate.spec.js
index fb3306409..cb2b3e592 100644
--- a/src/animations/animate.spec.js
+++ b/src/animations/animate.spec.js
@@ -1,6 +1,6 @@
import { createElementFromHTML, dealoc } from "../shared/dom.js";
import { Angular } from "../angular.js";
-import { isObject, mergeClasses } from "../shared/utils.js";
+import { isObject } from "../shared/utils.js";
import { isFunction, wait } from "../shared/utils.js";
import { createInjector } from "../core/di/injector.js";
@@ -496,8 +496,4 @@ describe("$animate", () => {
});
});
});
-
- describe("mergeClasses", () => {
- expect(mergeClasses);
- });
});
diff --git a/src/core/sanitize/sanitize-uri.js b/src/core/sanitize/sanitize-uri.js
index 90a8515a3..e98541df3 100644
--- a/src/core/sanitize/sanitize-uri.js
+++ b/src/core/sanitize/sanitize-uri.js
@@ -1,5 +1,4 @@
import { isDefined } from "../../shared/utils.js";
-import { urlResolve } from "../../shared/url-utils/url-utils.js";
/** @typedef {import('../../interface.ts').ServiceProvider} ServiceProvider */
@@ -55,21 +54,27 @@ export class SanitizeUriProvider {
/**
* @returns {import("./interface").SanitizerFn}
*/
- $get() {
- return (uri, isMediaUrl) => {
- if (!uri) return uri;
+ $get = [
+ "$window",
+ /** @param {ng.WindowService} $window */
+ ($window) => {
+ return /** @type {import("./interface").SanitizerFn} */ (
+ (uri, isMediaUrl) => {
+ if (!uri) return uri;
- /** @type {RegExp} */
- const regex = isMediaUrl
- ? this._imgSrcSanitizationTrustedUrlList
- : this._aHrefSanitizationTrustedUrlList;
+ /** @type {RegExp} */
+ const regex = isMediaUrl
+ ? this._imgSrcSanitizationTrustedUrlList
+ : this._aHrefSanitizationTrustedUrlList;
- const normalizedVal = urlResolve(uri.trim()).href;
+ const normalizedVal = new URL(uri.trim(), $window.location.href).href;
- if (normalizedVal !== "" && !normalizedVal.match(regex)) {
- return `unsafe:${normalizedVal}`;
- }
- return uri;
- };
- }
+ if (normalizedVal !== "" && !normalizedVal.match(regex)) {
+ return `unsafe:${normalizedVal}`;
+ }
+ return uri;
+ }
+ );
+ },
+ ];
}
diff --git a/src/core/sanitize/sanitize-uri.spec.js b/src/core/sanitize/sanitize-uri.spec.js
index 919b4dbfa..35d6c81fd 100644
--- a/src/core/sanitize/sanitize-uri.spec.js
+++ b/src/core/sanitize/sanitize-uri.spec.js
@@ -8,7 +8,7 @@ describe("sanitizeUri", () => {
let $$sanitizeUri;
beforeEach(() => {
sanitizeUriProvider = new SanitizeUriProvider();
- $$sanitizeUri = sanitizeUriProvider.$get();
+ $$sanitizeUri = sanitizeUriProvider.$get[1](window);
sanitizeHref = function (uri) {
return $$sanitizeUri(uri, false);
diff --git a/src/core/scope/scope.spec.js b/src/core/scope/scope.spec.js
index cc9663b52..c9af945e7 100644
--- a/src/core/scope/scope.spec.js
+++ b/src/core/scope/scope.spec.js
@@ -107,7 +107,7 @@ describe("Scope", () => {
});
describe("$nonscope", () => {
- it("should ignore objects with $nonscope propercoty", () => {
+ it("should ignore objects with $nonscope property", () => {
const res = createScope({ $nonscope: true });
expect(res.$id).toBeUndefined();
});
@@ -592,7 +592,7 @@ describe("Scope", () => {
expect(scope.counter).toBe(4);
});
- it("calls only the listeners registerred at the moment the watched value changes", async () => {
+ it("calls only the listeners registered at the moment the watched value changes", async () => {
scope.someValue = "a";
scope.counter = 0;
@@ -823,7 +823,7 @@ describe("Scope", () => {
expect(scope.$$watchersCount).toEqual(scope.$handler.watchers.size);
});
- it("should fire upon $watch registration on initial registeration", async () => {
+ it("should fire upon $watch registration on initial registration", async () => {
logs = "";
scope.a = 1;
scope.$watch("a", () => {
diff --git a/src/router/state/state.spec.js b/src/router/state/state.spec.js
index 30c73eba2..ad6829076 100644
--- a/src/router/state/state.spec.js
+++ b/src/router/state/state.spec.js
@@ -118,7 +118,7 @@ describe("$state", () => {
expect($stateProvider).toBeDefined();
});
- it("should should not allow states that are already registerred", () => {
+ it("should should not allow states that are already registered", () => {
expect(() => {
$stateProvider.state({ name: "toString", url: "/to-string" });
}).not.toThrow();
From 1671cef7bd0498dc348fba21bd042a5a845015d1 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sat, 15 Nov 2025 22:07:20 +0000
Subject: [PATCH 4/8] Bump js-yaml from 4.1.0 to 4.1.1
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.1.0 to 4.1.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)
---
updated-dependencies:
- dependency-name: js-yaml
dependency-version: 4.1.1
dependency-type: indirect
...
Signed-off-by: dependabot[bot]
---
package-lock.json | 40 ++++++++++++++++++++--------------------
1 file changed, 20 insertions(+), 20 deletions(-)
diff --git a/package-lock.json b/package-lock.json
index 20fcdd78e..783e27e9f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10,31 +10,31 @@
"license": "MIT",
"devDependencies": {
"@eslint/js": "^9.28.0",
- "@playwright/test": "latest",
- "@rollup/plugin-commonjs": "latest",
- "@rollup/plugin-node-resolve": "latest",
- "@rollup/plugin-terser": "latest",
- "@types/jasmine": "latest",
+ "@playwright/test": "*",
+ "@rollup/plugin-commonjs": "*",
+ "@rollup/plugin-node-resolve": "*",
+ "@rollup/plugin-terser": "*",
+ "@types/jasmine": "*",
"cssnano": "^7.1.1",
"eslint": "^9.28.0",
"eslint-config-prettier": "^10.1.5",
- "eslint-plugin-import": "latest",
+ "eslint-plugin-import": "*",
"eslint-plugin-prettier": "^5.4.1",
- "eslint-plugin-promise": "latest",
- "express": "latest",
+ "eslint-plugin-promise": "*",
+ "express": "*",
"globals": "^16.2.0",
"husky": "^9.1.7",
- "jasmine-core": "latest",
- "npm-run-all": "latest",
- "playwright": "latest",
+ "jasmine-core": "*",
+ "npm-run-all": "*",
+ "playwright": "*",
"postcss": "^8.5.6",
- "prettier": "latest",
- "rollup": "latest",
- "rollup-plugin-version-injector": "latest",
+ "prettier": "*",
+ "rollup": "*",
+ "rollup-plugin-version-injector": "*",
"sinon": "19.0.4",
- "typedoc": "latest",
- "typescript": "latest",
- "vite": "latest"
+ "typedoc": "*",
+ "typescript": "*",
+ "vite": "*"
},
"engines": {
"node": ">=24.0.0"
@@ -3926,9 +3926,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
- "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
From e0b847d1cb09c7f174b21c41253d51ba8426d0df Mon Sep 17 00:00:00 2001
From: Anatoly Ostrovsky
Date: Fri, 21 Nov 2025 10:21:01 +0200
Subject: [PATCH 5/8] Type improvements. Ignore events for scope
---
@types/animations/animate.d.ts | 74 ++--------------------
@types/core/compile/attributes.d.ts | 4 +-
@types/core/sanitize/interface.d.ts | 8 +++
@types/core/sanitize/sanitize-uri.d.ts | 7 ++-
@types/services/sce/sce.d.ts | 86 ++------------------------
src/animations/animate-example.html | 60 ++++++++++++++++++
src/animations/animate.js | 74 ++--------------------
src/core/compile/attributes.js | 2 +-
src/core/sanitize/interface.ts | 10 ++-
src/core/sanitize/sanitize-uri.js | 7 ++-
src/core/scope/scope.js | 5 ++
src/core/scope/scope.spec.js | 4 ++
src/services/sce/sce.js | 85 +------------------------
13 files changed, 112 insertions(+), 314 deletions(-)
create mode 100644 @types/core/sanitize/interface.d.ts
create mode 100644 src/animations/animate-example.html
diff --git a/@types/animations/animate.d.ts b/@types/animations/animate.d.ts
index d67a2d3b2..c49f6f1e3 100644
--- a/@types/animations/animate.d.ts
+++ b/@types/animations/animate.d.ts
@@ -212,74 +212,12 @@ export class AnimateProvider {
*/
enabled: any;
/**
- * Cancels the provided animation and applies the end state of the animation.
- * Note that this does not cancel the underlying operation, e.g. the setting of classes or
- * adding the element to the DOM.
- *
- * @param {import('./runner/animate-runner.js').AnimateRunner} runner An animation runner returned by an $animate function.
- *
- * @example
-
-
- angular.module('animationExample', []).component('cancelExample', {
- templateUrl: 'template.html',
- controller: function($element, $animate) {
- this.runner = null;
-
- this.addClass = function() {
- this.runner = $animate.addClass($element.querySelectorAll('div'), 'red');
- let ctrl = this;
- this.runner.finally(function() {
- ctrl.runner = null;
- });
- };
-
- this.removeClass = function() {
- this.runner = $animate.removeClass($element.querySelectorAll('div'), 'red');
- let ctrl = this;
- this.runner.finally(function() {
- ctrl.runner = null;
- });
- };
-
- this.cancel = function() {
- $animate.cancel(this.runner);
- };
- }
- });
-
-
-
-
-
-
-
-
-
CSS-Animated Text
-
-
-
-
-
-
- .red-add, .red-remove {
- transition: all 4s cubic-bezier(0.250, 0.460, 0.450, 0.940);
- }
-
- .red,
- .red-add.red-add-active {
- color: #FF0000;
- font-size: 40px;
- }
-
- .red-remove.red-remove-active {
- font-size: 10px;
- color: black;
- }
-
-
-
- */
+ * Cancels the provided animation and applies the end state of the animation.
+ * Note that this does not cancel the underlying operation, e.g. the setting of classes or
+ * adding the element to the DOM.
+ *
+ * @param {import('./runner/animate-runner.js').AnimateRunner} runner An animation runner returned by an $animate function.
+ */
cancel(
runner: import("./runner/animate-runner.js").AnimateRunner,
): void;
diff --git a/@types/core/compile/attributes.d.ts b/@types/core/compile/attributes.d.ts
index 899b7cb00..390584f87 100644
--- a/@types/core/compile/attributes.d.ts
+++ b/@types/core/compile/attributes.d.ts
@@ -1,7 +1,7 @@
export class Attributes {
static $nonscope: boolean;
/**
- * @param {ng.Scope} $rootScope
+ * @param {ng.RootScopeService} $rootScope
* @param {*} $animate
* @param {ng.ExceptionHandlerService} $exceptionHandler
* @param {*} $sce
@@ -9,7 +9,7 @@ export class Attributes {
* @param {Object} [attributesToCopy]
*/
constructor(
- $rootScope: ng.Scope,
+ $rootScope: ng.RootScopeService,
$animate: any,
$exceptionHandler: ng.ExceptionHandlerService,
$sce: any,
diff --git a/@types/core/sanitize/interface.d.ts b/@types/core/sanitize/interface.d.ts
new file mode 100644
index 000000000..0046947df
--- /dev/null
+++ b/@types/core/sanitize/interface.d.ts
@@ -0,0 +1,8 @@
+/**
+ * Sanitizer function that processes a URI string and optionally
+ * treats it as a media URL.
+ */
+export type SanitizerFn = (
+ uri: string | null | undefined,
+ isMediaUrl?: boolean,
+) => string | null | undefined;
diff --git a/@types/core/sanitize/sanitize-uri.d.ts b/@types/core/sanitize/sanitize-uri.d.ts
index 5f51d5156..1ca5acb40 100644
--- a/@types/core/sanitize/sanitize-uri.d.ts
+++ b/@types/core/sanitize/sanitize-uri.d.ts
@@ -33,8 +33,11 @@ export class SanitizeUriProvider implements ServiceProvider {
regexp?: RegExp | undefined,
): RegExp | SanitizeUriProvider;
/**
- * @returns {import("./interface").SanitizerFn}
+ * @returns {import("./interface.ts").SanitizerFn}
*/
- $get: (string | (($window: ng.WindowService) => any))[];
+ $get: (
+ | string
+ | (($window: ng.WindowService) => import("./interface.ts").SanitizerFn)
+ )[];
}
export type ServiceProvider = import("../../interface.ts").ServiceProvider;
diff --git a/@types/services/sce/sce.d.ts b/@types/services/sce/sce.d.ts
index 4c3a225ce..9fb472893 100644
--- a/@types/services/sce/sce.d.ts
+++ b/@types/services/sce/sce.d.ts
@@ -24,85 +24,7 @@ export namespace SCE_CONTEXTS {
* `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict
* Contextual Escaping (SCE)} services to AngularTS.
*
- * For an overview of this service and the functionnality it provides in AngularTS, see the main
- * page for {@link ng.$sce SCE}. The current page is targeted for developers who need to alter how
- * SCE works in their application, which shouldn't be needed in most cases.
- *
- *
- * AngularTS strongly relies on contextual escaping for the security of bindings: disabling or
- * modifying this might cause cross site scripting (XSS) vulnerabilities. For libraries owners,
- * changes to this service will also influence users, so be extra careful and document your changes.
- *
- *
- * Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of
- * the `$sce` service to customize the way Strict Contextual Escaping works in AngularTS. This is
- * because, while the `$sce` provides numerous shorthand methods, etc., you really only need to
- * override 3 core functions (`trustAs`, `getTrusted` and `valueOf`) to replace the way things
- * work because `$sce` delegates to `$sceDelegate` for these operations.
- *
- * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} to configure this service.
- *
- * The default instance of `$sceDelegate` should work out of the box with little pain. While you
- * can override it completely to change the behavior of `$sce`, the common case would
- * involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting
- * your own trusted and banned resource lists for trusting URLs used for loading AngularTS resources
- * such as templates. Refer {@link ng.$sceDelegateProvider#trustedResourceUrlList
- * $sceDelegateProvider.trustedResourceUrlList} and {@link
- * ng.$sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider.bannedResourceUrlList}
- */
-/**
- *
- * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate
- * $sceDelegate service}, used as a delegate for {@link ng.$sce Strict Contextual Escaping (SCE)}.
- *
- * The `$sceDelegateProvider` allows one to get/set the `trustedResourceUrlList` and
- * `bannedResourceUrlList` used to ensure that the URLs used for sourcing AngularTS templates and
- * other script-running URLs are safe (all places that use the `$sce.RESOURCE_URL` context). See
- * {@link ng.$sceDelegateProvider#trustedResourceUrlList
- * $sceDelegateProvider.trustedResourceUrlList} and
- * {@link ng.$sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider.bannedResourceUrlList},
- *
- * For the general details about this service in AngularTS, read the main page for {@link ng.$sce
- * Strict Contextual Escaping (SCE)}.
- *
- * **Example**: Consider the following case.
- *
- * - your app is hosted at url `http://myapp.example.com/`
- * - but some of your templates are hosted on other domains you control such as
- * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc.
- * - and you have an open redirect at `http://myapp.example.com/clickThru?...`.
- *
- * Here is what a secure configuration for this scenario might look like:
- *
- * ```
- * angular.module('myApp', []).config(function($sceDelegateProvider) {
- * $sceDelegateProvider.trustedResourceUrlList([
- * // Allow same origin resource loads.
- * 'self',
- * // Allow loading from our assets domain. Notice the difference between * and **.
- * 'http://srv*.assets.example.com/**'
- * ]);
- *
- * // The banned resource URL list overrides the trusted resource URL list so the open redirect
- * // here is blocked.
- * $sceDelegateProvider.bannedResourceUrlList([
- * 'http://myapp.example.com/clickThru**'
- * ]);
- * });
- * ```
- * Note that an empty trusted resource URL list will block every resource URL from being loaded, and will require
- * you to manually mark each one as trusted with `$sce.trustAsResourceUrl`. However, templates
- * requested by {@link ng.$templateRequest $templateRequest} that are present in
- * {@link ng.$templateCache $templateCache} will not go through this check. If you have a mechanism
- * to populate your templates in that cache at config time, then it is a good idea to remove 'self'
- * from the trusted resource URL lsit. This helps to mitigate the security impact of certain types
- * of issues, like for instance attacker-controlled `ng-includes`.
- */
-/**
- * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict
- * Contextual Escaping (SCE)} services to AngularTS.
- *
- * For an overview of this service and the functionnality it provides in AngularTS, see the main
+ * For an overview of this service and the functionality it provides in AngularTS, see the main
* page for {@link ng.$sce SCE}. The current page is targeted for developers who need to alter how
* SCE works in their application, which shouldn't be needed in most cases.
*
@@ -179,7 +101,7 @@ export namespace SCE_CONTEXTS {
export class SceDelegateProvider {
/**
*
- * @param {Array=} trustedResourceUrlList When provided, replaces the trustedResourceUrlList with
+ * @param {Array=} value When provided, replaces the trustedResourceUrlList with
* the value provided. This must be an array or null. A snapshot of this array is used so
* further changes to the array are ignored.
* Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
@@ -198,7 +120,7 @@ export class SceDelegateProvider {
* its origin with other apps! It is a good idea to limit it to only your application's directory.
*
*/
- trustedResourceUrlList: (value: any, ...args: any[]) => any[];
+ trustedResourceUrlList: (value?: any[] | undefined, ...args: any[]) => any[];
/**
*
* @param {Array=} bannedResourceUrlList When provided, replaces the `bannedResourceUrlList` with
@@ -226,7 +148,7 @@ export class SceDelegateProvider {
| string
| ((
$injector: ng.InjectorService,
- $$sanitizeUri: any,
+ $$sanitizeUri: import("../../core/sanitize/interface.ts").SanitizerFn,
$exceptionHandler: ng.ExceptionHandlerService,
) => {
trustAs: (type: string, trustedValue: any) => any;
diff --git a/src/animations/animate-example.html b/src/animations/animate-example.html
new file mode 100644
index 000000000..878706297
--- /dev/null
+++ b/src/animations/animate-example.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/animations/animate.js b/src/animations/animate.js
index 981c71358..fc9b67459 100644
--- a/src/animations/animate.js
+++ b/src/animations/animate.js
@@ -304,74 +304,12 @@ export function AnimateProvider($provide) {
enabled: $$animateQueue.enabled,
/**
- * Cancels the provided animation and applies the end state of the animation.
- * Note that this does not cancel the underlying operation, e.g. the setting of classes or
- * adding the element to the DOM.
- *
- * @param {import('./runner/animate-runner.js').AnimateRunner} runner An animation runner returned by an $animate function.
- *
- * @example
-
-
- angular.module('animationExample', []).component('cancelExample', {
- templateUrl: 'template.html',
- controller: function($element, $animate) {
- this.runner = null;
-
- this.addClass = function() {
- this.runner = $animate.addClass($element.querySelectorAll('div'), 'red');
- let ctrl = this;
- this.runner.finally(function() {
- ctrl.runner = null;
- });
- };
-
- this.removeClass = function() {
- this.runner = $animate.removeClass($element.querySelectorAll('div'), 'red');
- let ctrl = this;
- this.runner.finally(function() {
- ctrl.runner = null;
- });
- };
-
- this.cancel = function() {
- $animate.cancel(this.runner);
- };
- }
- });
-
-
-
-
-
-
-
-
-
CSS-Animated Text
-
-
-
-
-
-
- .red-add, .red-remove {
- transition: all 4s cubic-bezier(0.250, 0.460, 0.450, 0.940);
- }
-
- .red,
- .red-add.red-add-active {
- color: #FF0000;
- font-size: 40px;
- }
-
- .red-remove.red-remove-active {
- font-size: 10px;
- color: black;
- }
-
-
-
- */
+ * Cancels the provided animation and applies the end state of the animation.
+ * Note that this does not cancel the underlying operation, e.g. the setting of classes or
+ * adding the element to the DOM.
+ *
+ * @param {import('./runner/animate-runner.js').AnimateRunner} runner An animation runner returned by an $animate function.
+ */
cancel(runner) {
if (runner.cancel) {
runner.cancel();
diff --git a/src/core/compile/attributes.js b/src/core/compile/attributes.js
index 301e624c8..79ef8571d 100644
--- a/src/core/compile/attributes.js
+++ b/src/core/compile/attributes.js
@@ -21,7 +21,7 @@ export class Attributes {
static $nonscope = true;
/**
- * @param {ng.Scope} $rootScope
+ * @param {ng.RootScopeService} $rootScope
* @param {*} $animate
* @param {ng.ExceptionHandlerService} $exceptionHandler
* @param {*} $sce
diff --git a/src/core/sanitize/interface.ts b/src/core/sanitize/interface.ts
index fca01f011..0046947df 100644
--- a/src/core/sanitize/interface.ts
+++ b/src/core/sanitize/interface.ts
@@ -2,9 +2,7 @@
* Sanitizer function that processes a URI string and optionally
* treats it as a media URL.
*/
-export interface SanitizerFn {
- (
- uri: string | null | undefined,
- isMediaUrl?: boolean,
- ): string | null | undefined;
-}
+export type SanitizerFn = (
+ uri: string | null | undefined,
+ isMediaUrl?: boolean,
+) => string | null | undefined;
diff --git a/src/core/sanitize/sanitize-uri.js b/src/core/sanitize/sanitize-uri.js
index e98541df3..080c13eda 100644
--- a/src/core/sanitize/sanitize-uri.js
+++ b/src/core/sanitize/sanitize-uri.js
@@ -1,4 +1,5 @@
import { isDefined } from "../../shared/utils.js";
+import { $injectTokens } from "../../injection-tokens.js";
/** @typedef {import('../../interface.ts').ServiceProvider} ServiceProvider */
@@ -52,13 +53,13 @@ export class SanitizeUriProvider {
}
/**
- * @returns {import("./interface").SanitizerFn}
+ * @returns {import("./interface.ts").SanitizerFn}
*/
$get = [
- "$window",
+ $injectTokens.$window,
/** @param {ng.WindowService} $window */
($window) => {
- return /** @type {import("./interface").SanitizerFn} */ (
+ return /** @type {import("./interface.ts").SanitizerFn} */ (
(uri, isMediaUrl) => {
if (!uri) return uri;
diff --git a/src/core/scope/scope.js b/src/core/scope/scope.js
index 562b04de1..81a0a0b34 100644
--- a/src/core/scope/scope.js
+++ b/src/core/scope/scope.js
@@ -136,6 +136,11 @@ export function isUnsafeGlobal(target) {
return true;
}
+ // Events
+ if (typeof Event !== "undefined" && target instanceof Event) {
+ return true;
+ }
+
// Cross-origin or non-enumerable window objects
try {
return Object.prototype.toString.call(target) === "[object Window]";
diff --git a/src/core/scope/scope.spec.js b/src/core/scope/scope.spec.js
index c9af945e7..c2592b294 100644
--- a/src/core/scope/scope.spec.js
+++ b/src/core/scope/scope.spec.js
@@ -3058,4 +3058,8 @@ describe("isUnsafeGlobal", () => {
};
expect(() => isUnsafeGlobal(fakeCrossOrigin)).not.toThrow();
});
+
+ it("ignores events", () => {
+ expect(isUnsafeGlobal(new PointerEvent("test"))).toBeTrue();
+ });
});
diff --git a/src/services/sce/sce.js b/src/services/sce/sce.js
index 74cd62ebe..4c6a5e9d5 100644
--- a/src/services/sce/sce.js
+++ b/src/services/sce/sce.js
@@ -88,7 +88,7 @@ export function adjustMatcher(matcher) {
* `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict
* Contextual Escaping (SCE)} services to AngularTS.
*
- * For an overview of this service and the functionnality it provides in AngularTS, see the main
+ * For an overview of this service and the functionality it provides in AngularTS, see the main
* page for {@link ng.$sce SCE}. The current page is targeted for developers who need to alter how
* SCE works in their application, which shouldn't be needed in most cases.
*
@@ -115,85 +115,6 @@ export function adjustMatcher(matcher) {
* ng.$sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider.bannedResourceUrlList}
*/
-/**
- *
- * The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate
- * $sceDelegate service}, used as a delegate for {@link ng.$sce Strict Contextual Escaping (SCE)}.
- *
- * The `$sceDelegateProvider` allows one to get/set the `trustedResourceUrlList` and
- * `bannedResourceUrlList` used to ensure that the URLs used for sourcing AngularTS templates and
- * other script-running URLs are safe (all places that use the `$sce.RESOURCE_URL` context). See
- * {@link ng.$sceDelegateProvider#trustedResourceUrlList
- * $sceDelegateProvider.trustedResourceUrlList} and
- * {@link ng.$sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider.bannedResourceUrlList},
- *
- * For the general details about this service in AngularTS, read the main page for {@link ng.$sce
- * Strict Contextual Escaping (SCE)}.
- *
- * **Example**: Consider the following case.
- *
- * - your app is hosted at url `http://myapp.example.com/`
- * - but some of your templates are hosted on other domains you control such as
- * `http://srv01.assets.example.com/`, `http://srv02.assets.example.com/`, etc.
- * - and you have an open redirect at `http://myapp.example.com/clickThru?...`.
- *
- * Here is what a secure configuration for this scenario might look like:
- *
- * ```
- * angular.module('myApp', []).config(function($sceDelegateProvider) {
- * $sceDelegateProvider.trustedResourceUrlList([
- * // Allow same origin resource loads.
- * 'self',
- * // Allow loading from our assets domain. Notice the difference between * and **.
- * 'http://srv*.assets.example.com/**'
- * ]);
- *
- * // The banned resource URL list overrides the trusted resource URL list so the open redirect
- * // here is blocked.
- * $sceDelegateProvider.bannedResourceUrlList([
- * 'http://myapp.example.com/clickThru**'
- * ]);
- * });
- * ```
- * Note that an empty trusted resource URL list will block every resource URL from being loaded, and will require
- * you to manually mark each one as trusted with `$sce.trustAsResourceUrl`. However, templates
- * requested by {@link ng.$templateRequest $templateRequest} that are present in
- * {@link ng.$templateCache $templateCache} will not go through this check. If you have a mechanism
- * to populate your templates in that cache at config time, then it is a good idea to remove 'self'
- * from the trusted resource URL lsit. This helps to mitigate the security impact of certain types
- * of issues, like for instance attacker-controlled `ng-includes`.
- */
-
-/**
- * `$sceDelegate` is a service that is used by the `$sce` service to provide {@link ng.$sce Strict
- * Contextual Escaping (SCE)} services to AngularTS.
- *
- * For an overview of this service and the functionnality it provides in AngularTS, see the main
- * page for {@link ng.$sce SCE}. The current page is targeted for developers who need to alter how
- * SCE works in their application, which shouldn't be needed in most cases.
- *
- *
- * AngularTS strongly relies on contextual escaping for the security of bindings: disabling or
- * modifying this might cause cross site scripting (XSS) vulnerabilities. For libraries owners,
- * changes to this service will also influence users, so be extra careful and document your changes.
- *
- *
- * Typically, you would configure or override the {@link ng.$sceDelegate $sceDelegate} instead of
- * the `$sce` service to customize the way Strict Contextual Escaping works in AngularTS. This is
- * because, while the `$sce` provides numerous shorthand methods, etc., you really only need to
- * override 3 core functions (`trustAs`, `getTrusted` and `valueOf`) to replace the way things
- * work because `$sce` delegates to `$sceDelegate` for these operations.
- *
- * Refer {@link ng.$sceDelegateProvider $sceDelegateProvider} to configure this service.
- *
- * The default instance of `$sceDelegate` should work out of the box with little pain. While you
- * can override it completely to change the behavior of `$sce`, the common case would
- * involve configuring the {@link ng.$sceDelegateProvider $sceDelegateProvider} instead by setting
- * your own trusted and banned resource lists for trusting URLs used for loading AngularTS resources
- * such as templates. Refer {@link ng.$sceDelegateProvider#trustedResourceUrlList
- * $sceDelegateProvider.trustedResourceUrlList} and {@link
- * ng.$sceDelegateProvider#bannedResourceUrlList $sceDelegateProvider.bannedResourceUrlList}
- */
/**
*
* The `$sceDelegateProvider` provider allows developers to configure the {@link ng.$sceDelegate
@@ -250,7 +171,7 @@ export class SceDelegateProvider {
/**
*
- * @param {Array=} trustedResourceUrlList When provided, replaces the trustedResourceUrlList with
+ * @param {Array=} value When provided, replaces the trustedResourceUrlList with
* the value provided. This must be an array or null. A snapshot of this array is used so
* further changes to the array are ignored.
* Follow {@link ng.$sce#resourceUrlPatternItem this link} for a description of the items
@@ -312,7 +233,7 @@ export class SceDelegateProvider {
/**
*
* @param {ng.InjectorService} $injector
- * @param {*} $$sanitizeUri
+ * @param {import("../../core/sanitize/interface.ts").SanitizerFn} $$sanitizeUri
* @param {ng.ExceptionHandlerService} $exceptionHandler
* @returns
*/
From dc4cebe3b9a755e869fbcf9ba28ab0c9f4edb3aa Mon Sep 17 00:00:00 2001
From: Anatoly Ostrovsky
Date: Thu, 13 Nov 2025 01:39:30 +0200
Subject: [PATCH 6/8] Demo
---
@types/directive/wasm/wasm.d.ts | 3 +++
index.html | 28 +++++-----------------------
src/directive/wasm/math.ts | 8 ++++++++
src/directive/wasm/math.wasm | Bin 0 -> 109 bytes
src/directive/wasm/wasm.js | 17 +++++++++++++++++
src/ng.js | 2 ++
6 files changed, 35 insertions(+), 23 deletions(-)
create mode 100644 @types/directive/wasm/wasm.d.ts
create mode 100644 src/directive/wasm/math.ts
create mode 100644 src/directive/wasm/math.wasm
create mode 100644 src/directive/wasm/wasm.js
diff --git a/@types/directive/wasm/wasm.d.ts b/@types/directive/wasm/wasm.d.ts
new file mode 100644
index 000000000..757fb4578
--- /dev/null
+++ b/@types/directive/wasm/wasm.d.ts
@@ -0,0 +1,3 @@
+export function ngWasmDirective(): {
+ link: ($scope: any, $element: any, $attrs: any) => Promise;
+};
diff --git a/index.html b/index.html
index 4bc4fa906..7b707829b 100644
--- a/index.html
+++ b/index.html
@@ -98,28 +98,10 @@
-