@reckona/mreact-compat 0.0.137 → 0.0.139

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/dom-props.ts CHANGED
@@ -12,6 +12,8 @@ import type { SyntheticEvent } from "./event-types.js";
12
12
  import {
13
13
  isDangerousHtmlAttribute,
14
14
  isDangerousHtmlOptIn,
15
+ isSrcsetAttribute,
16
+ isUnsafeMetaRefreshContent,
15
17
  isUnsafeUrlAttribute,
16
18
  isUrlAttribute,
17
19
  } from "./url-safety.js";
@@ -29,28 +31,34 @@ export function applyProps(
29
31
  ): void {
30
32
  const preserveHydrationAttributes = options.preserveHydrationAttributes === true;
31
33
  const previous = getAppliedProps(element);
34
+ const nextProps = sanitizeMetaRefreshElementProps(element, props);
32
35
 
33
36
  if (previous === undefined && !preserveHydrationAttributes) {
34
- if (applyInitialRowProps(element, props)) {
35
- setAppliedProps(element, { props });
37
+ if (applyInitialRowProps(element, nextProps)) {
38
+ setAppliedProps(element, {
39
+ attributeNames: collectAttributeNames(nextProps),
40
+ props: nextProps,
41
+ });
36
42
  return;
37
43
  }
38
44
 
45
+ const attributeNames = collectAttributeNames(nextProps);
39
46
  setAppliedProps(element, {
40
- props,
41
- ...applyInitialProps(element, props, path, options),
47
+ attributeNames,
48
+ props: nextProps,
49
+ ...applyInitialProps(element, nextProps, path, options),
42
50
  });
43
51
  return;
44
52
  }
45
53
 
46
54
  const previousProps = previous?.props ?? {};
47
55
  let listeners = previous?.listeners;
48
- const previousAttributeNames = collectAttributeNames(previousProps);
49
- const nextAttributeNames = collectAttributeNames(props);
56
+ const previousAttributeNames = previous?.attributeNames ?? collectAttributeNames(previousProps);
57
+ const nextAttributeNames = collectAttributeNames(nextProps);
50
58
 
51
59
  if (!preserveHydrationAttributes) {
52
60
  for (const attributeName of previousAttributeNames) {
53
- if (!nextAttributeNames.has(attributeName)) {
61
+ if (!nextAttributeNames.includes(attributeName)) {
54
62
  if (attributeName === "style") {
55
63
  removePreviousStyle(element, previousProps.style, path, options);
56
64
  continue;
@@ -71,7 +79,7 @@ export function applyProps(
71
79
 
72
80
  if (listeners !== undefined) {
73
81
  for (const [name, appliedListener] of listeners) {
74
- const nextValue = props[name];
82
+ const nextValue = nextProps[name];
75
83
 
76
84
  if (nextValue !== appliedListener.handler) {
77
85
  listeners.delete(name);
@@ -79,7 +87,13 @@ export function applyProps(
79
87
  }
80
88
  }
81
89
 
82
- for (const [name, value] of Object.entries(props)) {
90
+ for (const name in nextProps) {
91
+ if (!Object.prototype.hasOwnProperty.call(nextProps, name)) {
92
+ continue;
93
+ }
94
+
95
+ const value = nextProps[name];
96
+
83
97
  if (name === "children" || name === "ref" || name === "key") {
84
98
  continue;
85
99
  }
@@ -117,6 +131,10 @@ export function applyProps(
117
131
  continue;
118
132
  }
119
133
 
134
+ if (/^on/i.test(name)) {
135
+ continue;
136
+ }
137
+
120
138
  const attributeName = toDomAttributeName(name);
121
139
 
122
140
  if (typeof value === "boolean" && isBooleanishStringAttribute(attributeName)) {
@@ -163,7 +181,8 @@ export function applyProps(
163
181
  }
164
182
 
165
183
  setAppliedProps(element, {
166
- props,
184
+ attributeNames: nextAttributeNames,
185
+ props: nextProps,
167
186
  ...(listeners === undefined ? {} : { listeners }),
168
187
  });
169
188
  }
@@ -220,6 +239,10 @@ function applyInitialProps(
220
239
  continue;
221
240
  }
222
241
 
242
+ if (/^on/i.test(name)) {
243
+ continue;
244
+ }
245
+
223
246
  const attributeName = toDomAttributeName(name);
224
247
 
225
248
  if (typeof value === "boolean" && isBooleanishStringAttribute(attributeName)) {
@@ -338,6 +361,13 @@ function applyAttribute(
338
361
  return;
339
362
  }
340
363
 
364
+ if (/^on/i.test(name)) {
365
+ if (element.hasAttribute(name) && !preserveHydrationAttributes) {
366
+ element.removeAttribute(name);
367
+ }
368
+ return;
369
+ }
370
+
341
371
  if (value === null || value === undefined || value === false) {
342
372
  if (element.hasAttribute(name) && !preserveHydrationAttributes) {
343
373
  reportRecoverable(
@@ -363,7 +393,10 @@ function applyAttribute(
363
393
  // value is unsafe we treat it as if it were null -- remove the
364
394
  // existing attribute, log a recoverable mismatch, and stop. This
365
395
  // matches react-dom's sanitizeURL posture.
366
- if (isUrlAttribute(name) && isUnsafeUrlAttribute(name, stringValue)) {
396
+ if (
397
+ (isUrlAttribute(name) || isSrcsetAttribute(name)) &&
398
+ isUnsafeUrlAttribute(name, stringValue)
399
+ ) {
367
400
  if (element.hasAttribute(name) && !preserveHydrationAttributes) {
368
401
  reportRecoverable(
369
402
  options,
@@ -530,15 +563,21 @@ function removePreviousStyle(
530
563
  }
531
564
  }
532
565
 
533
- function collectAttributeNames(props: Record<string, unknown>): Set<string> {
534
- const names = new Set<string>();
566
+ function collectAttributeNames(props: Record<string, unknown>): string[] {
567
+ const names: string[] = [];
568
+
569
+ for (const name in props) {
570
+ if (!Object.prototype.hasOwnProperty.call(props, name)) {
571
+ continue;
572
+ }
573
+
574
+ const value = props[name];
535
575
 
536
- for (const [name, value] of Object.entries(props)) {
537
576
  if (
538
577
  name === "children" ||
539
578
  name === "ref" ||
540
579
  name === "key" ||
541
- /^on[A-Z]/.test(name) ||
580
+ /^on/i.test(name) ||
542
581
  value === null ||
543
582
  value === undefined
544
583
  ) {
@@ -556,21 +595,49 @@ function collectAttributeNames(props: Record<string, unknown>): Set<string> {
556
595
  }
557
596
 
558
597
  if (name === "defaultValue") {
559
- names.add("value");
598
+ pushUniqueAttributeName(names, "value");
560
599
  continue;
561
600
  }
562
601
 
563
602
  if (name === "defaultChecked") {
564
- names.add("checked");
603
+ pushUniqueAttributeName(names, "checked");
565
604
  continue;
566
605
  }
567
606
 
568
- names.add(attributeName);
607
+ pushUniqueAttributeName(names, attributeName);
569
608
  }
570
609
 
571
610
  return names;
572
611
  }
573
612
 
613
+ function pushUniqueAttributeName(names: string[], name: string): void {
614
+ if (!names.includes(name)) {
615
+ names.push(name);
616
+ }
617
+ }
618
+
619
+ function sanitizeMetaRefreshElementProps(
620
+ element: Element,
621
+ props: Record<string, unknown>,
622
+ ): Record<string, unknown> {
623
+ if (element.tagName.toLowerCase() !== "meta") {
624
+ return props;
625
+ }
626
+
627
+ const httpEquiv = props["http-equiv"] ?? props.httpEquiv ?? element.getAttribute("http-equiv");
628
+ const content = props.content;
629
+ if (typeof httpEquiv !== "string" || typeof content !== "string") {
630
+ return props;
631
+ }
632
+ if (!isUnsafeMetaRefreshContent(httpEquiv, content)) {
633
+ return props;
634
+ }
635
+
636
+ const sanitized = { ...props };
637
+ delete sanitized.content;
638
+ return sanitized;
639
+ }
640
+
574
641
  function isBooleanishStringAttribute(name: string): boolean {
575
642
  const attributeName = toDomAttributeName(name).toLowerCase();
576
643
  return attributeName.startsWith("aria-") || BOOLEANISH_STRING_ATTRIBUTES.has(attributeName);
package/src/element.ts CHANGED
@@ -120,6 +120,43 @@ export function createElement<P extends Record<string, unknown>>(
120
120
  };
121
121
  }
122
122
 
123
+ export function createElementFromJsxConfig<P extends Record<string, unknown>>(
124
+ type: ElementType<P>,
125
+ config: (P & ReactReservedProps & { children?: ReactCompatNode }) | null,
126
+ keyArgument?: unknown,
127
+ ): ReactCompatElement<P> {
128
+ const normalizedType =
129
+ typeof type === "object" && type !== null ? normalizeElementType(type) : type;
130
+ const key = keyArgument !== undefined
131
+ ? String(keyArgument)
132
+ : config?.key === undefined ? null : String(config.key);
133
+ const ref = config?.ref ?? null;
134
+ const hasChildren = config !== null && config !== undefined && hasOwnProperty.call(config, "children");
135
+ const children = config?.children;
136
+ const copiedProps = copyElementProps(config, undefined, true);
137
+ const props = (typeof normalizedType === "string"
138
+ ? copiedProps
139
+ : applyDefaultProps(normalizedType, copiedProps)) as P & {
140
+ children?: ReactCompatNode;
141
+ };
142
+
143
+ if (hasChildren) {
144
+ props.children = children;
145
+ }
146
+
147
+ if (typeof normalizedType === "string") {
148
+ setHostOwnPropsMeta(props);
149
+ }
150
+
151
+ return {
152
+ $$typeof: REACT_COMPAT_ELEMENT_TYPE,
153
+ type: normalizedType as ElementType<P>,
154
+ key,
155
+ ref,
156
+ props,
157
+ };
158
+ }
159
+
123
160
  export function isReactCompatElement(
124
161
  value: unknown,
125
162
  ): value is ReactCompatElement {
@@ -232,6 +269,7 @@ export function cloneElement<P extends Record<string, unknown>>(
232
269
  function copyElementProps(
233
270
  source: Record<string, unknown> | null | undefined,
234
271
  base?: Record<string, unknown>,
272
+ omitChildren = false,
235
273
  ): Record<string, unknown> {
236
274
  const props: Record<string, unknown> = base === undefined ? {} : { ...base };
237
275
 
@@ -244,7 +282,13 @@ function copyElementProps(
244
282
  continue;
245
283
  }
246
284
 
247
- if (name !== "key" && name !== "ref" && name !== "__self" && name !== "__source") {
285
+ if (
286
+ name !== "key" &&
287
+ name !== "ref" &&
288
+ name !== "__self" &&
289
+ name !== "__source" &&
290
+ (!omitChildren || name !== "children")
291
+ ) {
248
292
  props[name] = source[name];
249
293
  }
250
294
  }
@@ -1,6 +1,7 @@
1
1
  import type { SyntheticEvent } from "./event-types.js";
2
2
 
3
3
  export interface AppliedProps {
4
+ attributeNames?: string[];
4
5
  props: Record<string, unknown>;
5
6
  listeners?: Map<string, AppliedEventListener>;
6
7
  }
package/src/hooks.ts CHANGED
@@ -16,6 +16,7 @@ import { isThenable } from "./thenable.js";
16
16
  export interface RootRuntime {
17
17
  currentElement?: unknown;
18
18
  instances: Map<string, ComponentInstance>;
19
+ instanceKeysByPrefix: Map<string, Set<string>>;
19
20
  activeInstanceKeys: Set<string> | undefined;
20
21
  activeProfilerPaths: Set<string> | undefined;
21
22
  mountedProfilerPaths: Set<string>;
@@ -55,8 +56,8 @@ interface ComponentInstance {
55
56
  dirty: boolean;
56
57
  disposed?: boolean;
57
58
  contextDependencies?: Map<ReactCompatContextLike<unknown>, unknown>;
58
- devToolsHooks: DevToolsHookValue[];
59
- devToolsHookTypes: string[];
59
+ devToolsHooks?: DevToolsHookValue[];
60
+ devToolsHookTypes?: string[];
60
61
  devToolsHookSuppressionDepth: number;
61
62
  }
62
63
 
@@ -297,6 +298,7 @@ export function createRootRuntime(
297
298
  ): RootRuntime {
298
299
  return {
299
300
  instances: new Map(),
301
+ instanceKeysByPrefix: new Map(),
300
302
  activeInstanceKeys: undefined,
301
303
  activeProfilerPaths: undefined,
302
304
  mountedProfilerPaths: new Set(),
@@ -491,20 +493,24 @@ export function renderWithRootRuntime<T>(
491
493
  hooks: [],
492
494
  hookIndex: 0,
493
495
  dirty: false,
494
- devToolsHooks: [],
495
- devToolsHookTypes: [],
496
496
  devToolsHookSuppressionDepth: 0,
497
497
  };
498
498
  instance.owner = owner;
499
499
  instance.path = path;
500
500
  runtime.instances.set(path, instance);
501
+ indexInstanceKey(runtime, path);
501
502
  runtime.activeInstanceKeys?.add(path);
502
503
  instance.hookIndex = 0;
503
504
  instance.dirty = false;
504
505
  instance.disposed = false;
505
506
  delete instance.contextDependencies;
506
- instance.devToolsHooks = [];
507
- instance.devToolsHookTypes = [];
507
+ if (hasInstalledDevToolsHook()) {
508
+ instance.devToolsHooks = [];
509
+ instance.devToolsHookTypes = [];
510
+ } else {
511
+ delete instance.devToolsHooks;
512
+ delete instance.devToolsHookTypes;
513
+ }
508
514
  instance.devToolsHookSuppressionDepth = 0;
509
515
  hookRenderState.currentRuntime = runtime;
510
516
  hookRenderState.currentInstance = instance;
@@ -547,13 +553,35 @@ export function hasContextDependency(
547
553
  return keys.some((key) => runtime.instances.get(key)?.contextDependencies !== undefined);
548
554
  }
549
555
 
556
+ export function collectRuntimeInstanceKeys(runtime: RootRuntime, prefix: string): string[] {
557
+ const keys = runtime.instanceKeysByPrefix.get(prefix);
558
+
559
+ if (keys === undefined) {
560
+ return [];
561
+ }
562
+
563
+ const activeKeys: string[] = [];
564
+
565
+ for (const key of keys) {
566
+ if (runtime.instances.has(key)) {
567
+ activeKeys.push(key);
568
+ }
569
+ }
570
+
571
+ return activeKeys;
572
+ }
573
+
550
574
  export function getDevToolsHookState(
551
575
  runtime: RootRuntime,
552
576
  path: string,
553
577
  ): DevToolsHookState | undefined {
554
578
  const instance = runtime.instances.get(path);
555
579
 
556
- if (instance === undefined) {
580
+ if (
581
+ instance === undefined ||
582
+ instance.devToolsHooks === undefined ||
583
+ instance.devToolsHookTypes === undefined
584
+ ) {
557
585
  return undefined;
558
586
  }
559
587
 
@@ -966,7 +994,12 @@ function assignRef<T>(ref: unknown, value: T | null): void {
966
994
  function recordDevToolsHook(type: string, value: DevToolsHookValue): void {
967
995
  const instance = hookRenderState.currentInstance;
968
996
 
969
- if (instance === undefined || instance.devToolsHookSuppressionDepth > 0) {
997
+ if (
998
+ instance === undefined ||
999
+ instance.devToolsHookSuppressionDepth > 0 ||
1000
+ instance.devToolsHooks === undefined ||
1001
+ instance.devToolsHookTypes === undefined
1002
+ ) {
970
1003
  return;
971
1004
  }
972
1005
 
@@ -974,6 +1007,12 @@ function recordDevToolsHook(type: string, value: DevToolsHookValue): void {
974
1007
  instance.devToolsHooks.push(value);
975
1008
  }
976
1009
 
1010
+ function hasInstalledDevToolsHook(): boolean {
1011
+ return typeof (globalThis as {
1012
+ __REACT_DEVTOOLS_GLOBAL_HOOK__?: { inject?: unknown } | undefined;
1013
+ }).__REACT_DEVTOOLS_GLOBAL_HOOK__?.inject === "function";
1014
+ }
1015
+
977
1016
  function runWithoutDevToolsHookTracking<T>(callback: () => T): T {
978
1017
  const instance = requireInstance();
979
1018
  instance.devToolsHookSuppressionDepth += 1;
@@ -1941,10 +1980,51 @@ function cleanupInactiveInstances(runtime: RootRuntime): void {
1941
1980
  if (!activeInstanceKeys.has(key)) {
1942
1981
  cleanupInstance(instance);
1943
1982
  runtime.instances.delete(key);
1983
+ removeInstanceKeyFromIndex(runtime, key);
1984
+ }
1985
+ }
1986
+ }
1987
+
1988
+ function indexInstanceKey(runtime: RootRuntime, key: string): void {
1989
+ for (const prefix of instanceKeyPrefixes(key)) {
1990
+ let keys = runtime.instanceKeysByPrefix.get(prefix);
1991
+
1992
+ if (keys === undefined) {
1993
+ keys = new Set();
1994
+ runtime.instanceKeysByPrefix.set(prefix, keys);
1944
1995
  }
1996
+
1997
+ keys.add(key);
1945
1998
  }
1946
1999
  }
1947
2000
 
2001
+ function removeInstanceKeyFromIndex(runtime: RootRuntime, key: string): void {
2002
+ for (const prefix of instanceKeyPrefixes(key)) {
2003
+ const keys = runtime.instanceKeysByPrefix.get(prefix);
2004
+
2005
+ if (keys === undefined) {
2006
+ continue;
2007
+ }
2008
+
2009
+ keys.delete(key);
2010
+
2011
+ if (keys.size === 0) {
2012
+ runtime.instanceKeysByPrefix.delete(prefix);
2013
+ }
2014
+ }
2015
+ }
2016
+
2017
+ function instanceKeyPrefixes(key: string): string[] {
2018
+ const parts = key.split(".");
2019
+ const prefixes: string[] = [];
2020
+
2021
+ for (let index = 1; index <= parts.length; index += 1) {
2022
+ prefixes.push(parts.slice(0, index).join("."));
2023
+ }
2024
+
2025
+ return prefixes;
2026
+ }
2027
+
1948
2028
  function cleanupInstance(instance: ComponentInstance): void {
1949
2029
  instance.disposed = true;
1950
2030
  for (const slot of instance.hooks) {
@@ -45,6 +45,7 @@ import {
45
45
  restoreRuntimeSnapshot,
46
46
  takeRuntimeSnapshot,
47
47
  getDevToolsHookState,
48
+ collectRuntimeInstanceKeys,
48
49
  hasContextDependency,
49
50
  hasChangedContextDependency,
50
51
  type RootRuntime,
@@ -3074,9 +3075,7 @@ function isLazyType(
3074
3075
  }
3075
3076
 
3076
3077
  function collectInstanceKeys(runtime: RootRuntime, prefix: string): string[] {
3077
- return Array.from(runtime.instances.keys()).filter(
3078
- (key) => key === prefix || key.startsWith(`${prefix}.`),
3079
- );
3078
+ return collectRuntimeInstanceKeys(runtime, prefix);
3080
3079
  }
3081
3080
 
3082
3081
  function markActiveInstanceKeys(runtime: RootRuntime, keys: readonly string[]): void {
@@ -1,4 +1,4 @@
1
- import { createElement, Fragment } from "./element.js";
1
+ import { createElementFromJsxConfig, Fragment } from "./element.js";
2
2
  import type {
3
3
  ElementType,
4
4
  ReactCompatElement,
@@ -99,21 +99,5 @@ function createElementFromJsx<P extends Record<string, unknown>>(
99
99
  props: (P & { children?: ReactCompatNode; key?: unknown; ref?: unknown }) | null,
100
100
  key: unknown,
101
101
  ): ReactCompatElement<P> {
102
- const config = { ...props } as P & {
103
- children?: ReactCompatNode;
104
- key?: unknown;
105
- ref?: unknown;
106
- };
107
- const hasChildren = Object.hasOwn(config, "children");
108
- const children = config.children;
109
-
110
- if (key !== undefined) {
111
- config.key = key;
112
- }
113
-
114
- delete config.children;
115
-
116
- return hasChildren
117
- ? createElement(type, config, children)
118
- : createElement(type, config);
102
+ return createElementFromJsxConfig(type, props, key);
119
103
  }
package/src/reconciler.ts CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  } from "./context.js";
25
25
  import {
26
26
  clearRuntimePortalNodes,
27
+ collectRuntimeInstanceKeys,
27
28
  hasStableExternalStores,
28
29
  restoreRuntimeSnapshot,
29
30
  renderWithProfiler,
@@ -684,9 +685,7 @@ function getMemoRenderStates(runtime: RootRuntime): Map<string, MemoRenderState>
684
685
  }
685
686
 
686
687
  function collectInstanceKeys(runtime: RootRuntime, prefix: string): string[] {
687
- return Array.from(runtime.instances.keys()).filter((key) =>
688
- key === prefix || key.startsWith(`${prefix}.`),
689
- );
688
+ return collectRuntimeInstanceKeys(runtime, prefix);
690
689
  }
691
690
 
692
691
  function markActiveInstanceKeys(runtime: RootRuntime, keys: readonly string[]): void {
@@ -25,7 +25,12 @@ import {
25
25
  type RootRuntime,
26
26
  type RootRuntimeOptions,
27
27
  } from "./hooks.js";
28
- import { isDangerousHtmlAttribute, isDangerousHtmlOptIn } from "./url-safety.js";
28
+ import {
29
+ isDangerousHtmlAttribute,
30
+ isDangerousHtmlOptIn,
31
+ isUnsafeMetaRefreshContent,
32
+ isUnsafeUrlAttribute,
33
+ } from "./url-safety.js";
29
34
  import { escapeHtmlAttribute as escapeHtml } from "@reckona/mreact-shared/html-escape";
30
35
  import { isVoidHtmlElement } from "@reckona/mreact-shared";
31
36
 
@@ -195,7 +200,8 @@ function renderElementToString(
195
200
  }
196
201
 
197
202
  function renderAttributesToString(props: Record<string, unknown>): string {
198
- const entries = Object.entries(props);
203
+ const sanitizedProps = sanitizeMetaRefreshProps(props);
204
+ const entries = Object.entries(sanitizedProps);
199
205
  if (
200
206
  entries.length === 0 ||
201
207
  (entries.length === 1 && entries[0]?.[0] === "children")
@@ -210,6 +216,23 @@ function renderAttributesToString(props: Record<string, unknown>): string {
210
216
  return attributes;
211
217
  }
212
218
 
219
+ function sanitizeMetaRefreshProps(
220
+ props: Record<string, unknown>,
221
+ ): Record<string, unknown> {
222
+ const httpEquiv = props["http-equiv"] ?? props.httpEquiv;
223
+ const content = props.content;
224
+ if (typeof httpEquiv !== "string" || typeof content !== "string") {
225
+ return props;
226
+ }
227
+ if (!isUnsafeMetaRefreshContent(httpEquiv, content)) {
228
+ return props;
229
+ }
230
+
231
+ const sanitized = { ...props };
232
+ delete sanitized.content;
233
+ return sanitized;
234
+ }
235
+
213
236
  function isClassComponentType(
214
237
  value: unknown,
215
238
  ): value is new (props: Record<string, unknown>) => { render(): ReactCompatNode } {
@@ -305,7 +328,7 @@ function renderHtmlAttribute(name: string, value: unknown): string {
305
328
  name === "children" ||
306
329
  name === "key" ||
307
330
  name === "ref" ||
308
- /^on[A-Z]/.test(name) ||
331
+ /^on/i.test(name) ||
309
332
  value === null ||
310
333
  value === undefined ||
311
334
  typeof value === "function"
@@ -320,6 +343,14 @@ function renderHtmlAttribute(name: string, value: unknown): string {
320
343
 
321
344
  const attributeName = toHtmlAttributeName(name);
322
345
 
346
+ if (!VALID_ATTRIBUTE_NAME.test(attributeName)) {
347
+ return "";
348
+ }
349
+
350
+ if (/^on/i.test(attributeName)) {
351
+ return "";
352
+ }
353
+
323
354
  if (typeof value === "boolean" && isBooleanishStringAttribute(attributeName)) {
324
355
  return ` ${attributeName}="${value ? "true" : "false"}"`;
325
356
  }
@@ -346,9 +377,17 @@ function renderHtmlAttribute(name: string, value: unknown): string {
346
377
  return ` ${attributeName}=""`;
347
378
  }
348
379
 
349
- return ` ${attributeName}="${escapeHtml(value)}"`;
380
+ const stringValue = String(value);
381
+
382
+ if (isUnsafeUrlAttribute(attributeName, stringValue)) {
383
+ return "";
384
+ }
385
+
386
+ return ` ${attributeName}="${escapeHtml(stringValue)}"`;
350
387
  }
351
388
 
389
+ const VALID_ATTRIBUTE_NAME = /^[A-Za-z_][\w.\-:]*$/;
390
+
352
391
  function isBooleanishStringAttribute(name: string): boolean {
353
392
  const attributeName = toHtmlAttributeName(name).toLowerCase();
354
393
  return attributeName.startsWith("aria-") || BOOLEANISH_STRING_ATTRIBUTES.has(attributeName);
package/src/url-safety.ts CHANGED
@@ -2,6 +2,7 @@ export {
2
2
  isDangerousHtmlAttribute,
3
3
  isDangerousHtmlOptIn,
4
4
  isSrcsetAttribute,
5
+ isUnsafeMetaRefreshContent,
5
6
  isUnsafeUrlAttribute,
6
7
  isUrlAttribute,
7
8
  } from "@reckona/mreact-shared/url-safety";