Repository: remarkablemark/html-react-parser (ts)

Issue:
Add Current Node Index in `replace` Callback
## Problem
I'm working with `HTML/JS` code fetched from a CMS. This code may contain multiple scripts. I'm using `next/script` which requires each script to have a unique id. However, when there are many scripts, it's challenging to assign the correct id since the scripts are dynamically mounted.

Here's how I'm currently handling scripts:

```ts
parser(html, {
  replace(domNode) {
    if (domNode instanceof Element && domNode.type === "script") {
      const __html = domNode.children
        .filter((child) => child instanceof Text)
        .map((child) => (child as Text).data)
        .join("");

      const props = attributesToProps(domNode.attribs);

      return (
        <Script
          {...props}
          dangerouslySetInnerHTML={{
            __html
          }}
        />
      );
    }
  }
});
```

I could assign a unique id to each script, but this would require handling many cases and tracking which script has the current id and which script is currently mounting to avoid id duplication.

so I’ve come up with this solution:
```ts
import React from "react";

import Script from "next/script";

import parser, { Element, Text, attributesToProps, htmlToDOM } from "html-react-parser";

function parseHTML(html: string | null | undefined, fixedID: string) {
  //get all script tags and map them to an array with their index
  const scripts = new Set(
    html
      ? htmlToDOM(html)
          .filter((node) => node instanceof Element && node.type === "script")
          .map((_, index) => index)
      : []
  );

  return (
    html &&
    parser(html, {
      replace(domNode) {
        if (domNode instanceof Element && domNode.type === "script") {
          const __html = domNode.children
            .filter((child) => child instanceof Text)
            .map((child) => (child as Text).data)
            .join("");

          const props = attributesToProps(domNode.attribs);

          //get the first script tag and remove it from the set
          const id = `script-${scripts.values().next().value}`;
          scripts.delete(Number(id.replace("script-", "")));

          return (
            <Script
              {...props}
              id={`${fixedID}-${id}`}
              dangerouslySetInnerHTML={{
                __html
              }}
            />
          );
        }
      }
    })
  );
}

export default parseHTML;
```
I can’t use `useMemo` in scripts because it’s not in a React component or a hook. So, each time a re-render happens, the scripts will be recalculated. Also, I think that looping through the DOM to filter only the script tags and map them into an index may cause performance issues because the `html` may be large or contain many elements, and each time a re-render happens, the code will run again.


## Suggested Solution
the best solution is adding the current node index as a parameter in `replace` callback. so I can easily have a unique id and have smth like: `const cacheKey = ${fixedID}-${index}`. 

## Keywords

`next/script`, dynamic script handling, unique id, replace.  


Pull request description:
feat(dom-to-react): pass index as 2nd argument to `replace` option
## What is the motivation for this pull request?

feat(dom-to-react): pass index as 2nd argument to `replace` option

Closes #1240

## What is the current behavior?

`replace` option has 1 argument: `domNode`

## What is the new behavior?

`replace` option has 2 arguments: `domNode` and `index`

## Checklist:

- [x] [Conventional Commits](https://www.conventionalcommits.org/)
- [x] Tests
- [x] Documentation

Patch:
diff --git a/README.md b/README.md
index d5a115f..e3f3978 100644
--- a/README.md
+++ b/README.md
@@ -196,6 +196,19 @@ parse('<p id="replace">text</p>', {
 });
 ```
 
+The second argument is the index:
+
+```ts
+parse('<br>', {
+  replace(domNode, index) {
+    console.assert(typeof index === 'number');
+  },
+});
+```
+
+> [!NOTE]
+> The index will restart at 0 when traversing the node's children so don't rely on index being a unique key.
+
 #### replace with TypeScript
 
 For TypeScript, you'll need to check that `domNode` is an instance of domhandler's `Element`:
diff --git a/src/dom-to-react.ts b/src/dom-to-react.ts
index 47804e5..3af21d3 100644
--- a/src/dom-to-react.ts
+++ b/src/dom-to-react.ts
@@ -44,7 +44,7 @@ export default function domToReact(
 
     // replace with custom React element (if present)
     if (hasReplace) {
-      let replaceElement = options.replace!(node) as JSX.Element;
+      let replaceElement = options.replace!(node, index) as JSX.Element;
 
       if (isValidElement(replaceElement)) {
         // set "key" prop for sibling elements
diff --git a/src/types.ts b/src/types.ts
index d032410..1503ed3 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -19,6 +19,7 @@ export interface HTMLReactParserOptions {
 
   replace?: (
     domNode: DOMNode,
+    index: number,
   ) => JSX.Element | string | null | boolean | object | void;
 
   transform?: (


Test patch:
diff --git a/__tests__/dom-to-react.test.tsx b/__tests__/dom-to-react.test.tsx
index 92cafb5..8ec21be 100644
--- a/__tests__/dom-to-react.test.tsx
+++ b/__tests__/dom-to-react.test.tsx
@@ -182,14 +182,14 @@ describe('replace option', () => {
     },
   );
 
-  it("does not set key if there's a single node", () => {
+  it('does not set key for a single node', () => {
     const reactElement = domToReact(htmlToDOM(html.single), {
       replace: () => <div />,
     }) as JSX.Element;
     expect(reactElement.key).toBe(null);
   });
 
-  it("does not modify keys if they're already set", () => {
+  it('does not modify keys if they are already set', () => {
     const reactElements = domToReact(
       htmlToDOM(html.single + html.customElement),
       {
@@ -230,6 +230,15 @@ describe('replace option', () => {
     const reactElement = domToReact(htmlToDOM('<div>test</div>'), options);
     expect(reactElement).toEqual(<div>test</div>);
   });
+
+  it('passes index as the 2nd argument', () => {
+    const reactElement = domToReact(htmlToDOM('<li>one</li><li>two</li>'), {
+      replace(domNode, index) {
+        expect(typeof index).toBe('number');
+      },
+    });
+    expect(reactElement).toHaveLength(2);
+  });
 });
 
 describe('transform option', () => {