fix(js_interop_gen): address PR review feedback and add cycle guards

- Use cloneType in getLowestCommonAncestorOfTypes to prevent in-place nullability mutation.
- Add img, hr, details, summary to allowed HTML tags in doc comment formatting.
- Add visited set cycle guards to getMemberHierarchy and findMemberInHierarchy.
- Add unit tests in generate_docs_test and type_map_test.
diff --git a/js_interop_gen/lib/src/ast/helpers.dart b/js_interop_gen/lib/src/ast/helpers.dart
index 19f8cb9..5021f2c 100644
--- a/js_interop_gen/lib/src/ast/helpers.dart
+++ b/js_interop_gen/lib/src/ast/helpers.dart
@@ -83,12 +83,15 @@
 Set<String> getMemberHierarchy(
   Declaration type, [
   bool addDirectMembers = false,
+  Set<Declaration>? visited,
 ]) {
+  visited ??= {};
+  if (!visited.add(type)) return {};
   final members = <String>{};
 
   void addMembersIfReferredType(Type type) {
     if (type case ReferredType<Declaration>(declaration: final d)) {
-      members.addAll(getMemberHierarchy(d, true));
+      members.addAll(getMemberHierarchy(d, true, visited));
     }
   }
 
@@ -136,7 +139,13 @@
   return members;
 }
 
-MemberDeclaration? findMemberInHierarchy(Declaration td, String name) {
+MemberDeclaration? findMemberInHierarchy(
+  Declaration td,
+  String name, [
+  Set<Declaration>? visited,
+]) {
+  visited ??= {};
+  if (!visited.add(td)) return null;
   if (td is TypeDeclaration) {
     // Check direct members first
     final prop = td.properties.where((p) => p.name == name).firstOrNull;
@@ -157,7 +166,7 @@
 
     for (final parent in parents) {
       if (parent case ReferredType(declaration: final d)) {
-        final found = findMemberInHierarchy(d, name);
+        final found = findMemberInHierarchy(d, name, visited);
         if (found != null) return found;
       }
     }
@@ -178,7 +187,7 @@
     // Check constituents recursively
     for (final parent in td.types) {
       if (parent case ReferredType(declaration: final d)) {
-        final found = findMemberInHierarchy(d, name);
+        final found = findMemberInHierarchy(d, name, visited);
         if (found != null) return found;
       }
     }
diff --git a/js_interop_gen/lib/src/formatting.dart b/js_interop_gen/lib/src/formatting.dart
index db31eee..0de8ba9 100644
--- a/js_interop_gen/lib/src/formatting.dart
+++ b/js_interop_gen/lib/src/formatting.dart
@@ -24,6 +24,10 @@
   'h5',
   'h6',
   'blockquote',
+  'img',
+  'hr',
+  'details',
+  'summary',
   '/a',
   '/b',
   '/i',
@@ -45,6 +49,8 @@
   '/h5',
   '/h6',
   '/blockquote',
+  '/details',
+  '/summary',
 };
 
 /// Given markdown formatted text [data] and a line [width], return a
diff --git a/js_interop_gen/lib/src/interop_gen/sub_type.dart b/js_interop_gen/lib/src/interop_gen/sub_type.dart
index 149d6ab..0eb4974 100644
--- a/js_interop_gen/lib/src/interop_gen/sub_type.dart
+++ b/js_interop_gen/lib/src/interop_gen/sub_type.dart
@@ -410,7 +410,7 @@
 
   if (types.isEmpty) throw Exception('You must pass types');
   if (types.singleOrNull case final singleType?) {
-    return singleType..isNullable = isNullable;
+    return cloneType(singleType, isNullable: isNullable);
   }
 
   if (_getSharedPrimitiveTypeIfAny(types, isNullable: isNullable)
diff --git a/js_interop_gen/test/generate_docs_test.dart b/js_interop_gen/test/generate_docs_test.dart
index bd642fb..8557ea0 100644
--- a/js_interop_gen/test/generate_docs_test.dart
+++ b/js_interop_gen/test/generate_docs_test.dart
@@ -122,6 +122,15 @@
 ''',
       );
     });
+
+    test('allowed HTML tags (img, hr, details, summary)', () {
+      compare(
+        '<img src="test.png"> <hr> <details><summary>Click</summary>Info</details>',
+        '''
+/// <img src="test.png"> <hr> <details><summary>Click</summary>Info</details>
+''',
+      );
+    });
   });
 }
 
diff --git a/js_interop_gen/test/type_map_test.dart b/js_interop_gen/test/type_map_test.dart
index 0bd5f42..75095c1 100644
--- a/js_interop_gen/test/type_map_test.dart
+++ b/js_interop_gen/test/type_map_test.dart
@@ -8,6 +8,7 @@
 import 'package:js_interop_gen/src/ast/base.dart';
 import 'package:js_interop_gen/src/ast/builtin.dart';
 import 'package:js_interop_gen/src/ast/declarations.dart';
+import 'package:js_interop_gen/src/ast/helpers.dart';
 import 'package:js_interop_gen/src/ast/types.dart';
 import 'package:js_interop_gen/src/interop_gen/namer.dart';
 import 'package:js_interop_gen/src/interop_gen/sub_type.dart';
@@ -524,6 +525,33 @@
         final hierarchy = getTypeHierarchy(myEnum.asReferredType());
         expect(hierarchy.lookup('JSNumber'), isNotNull);
       });
+
+      test('Circular type hierarchy cycle guard in helpers', () {
+        final dummy = InterfaceDeclaration(
+          name: 'Dummy',
+          exported: true,
+          id: ID(type: 'interface', name: 'Dummy'),
+        );
+        late final InterfaceDeclaration a;
+        late final InterfaceDeclaration b;
+
+        a = InterfaceDeclaration(
+          name: 'CircA',
+          exported: true,
+          id: ID(type: 'interface', name: 'CircA'),
+          extendedTypes: [ReferredType(name: 'CircB', declaration: dummy)],
+        );
+        b = InterfaceDeclaration(
+          name: 'CircB',
+          exported: true,
+          id: ID(type: 'interface', name: 'CircB'),
+          extendedTypes: [ReferredType(name: 'CircA', declaration: a)],
+        );
+        (a.extendedTypes.first as ReferredType).declaration = b;
+
+        expect(() => getMemberHierarchy(a), returnsNormally);
+        expect(() => findMemberInHierarchy(a, 'someProp'), returnsNormally);
+      });
     });
   });
 }