This is an automated email from the ASF dual-hosted git repository.

tbonelee pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new db540a1eb5 [ZEPPELIN-6500] Replace ansi-to-react with anser in the 
React text re…
db540a1eb5 is described below

commit db540a1eb585dd21bcfb0ce6c7c1840d0a670f39
Author: 김다은 (Daeun Kim) <[email protected]>
AuthorDate: Mon Oct 5 22:29:47 2026 +0900

    [ZEPPELIN-6500] Replace ansi-to-react with anser in the React text re…
    
    ### What is this PR for?
    `projects/zeppelin-react` rendered ANSI-colored interpreter output through 
`ansi-to-react`, which comes from the archived nteract project and declares 
`linkify-it<at>^3.0.3` (GHSA-22p9-wv53-3rq4). The fix only exists in linkify-it 
5.x, so PR #5278 worked around it with an npm `overrides` entry that crosses 
`ansi-to-react`'s declared range. Nothing would ever prompt us to remove that 
override.
    
    `linkify-it` only backs `ansi-to-react`'s optional `linkify` prop, which 
`TextRenderer.tsx` never enabled, and the actual ANSI parsing is done by 
`anser`. This renders `anser.ansiToJson()` output directly, which removes 
`ansi-to-react`, `linkify-it`, `escape-carriage`, `uc.micro` and the 
`linkify-it` override from the tree.
    
    - The rendered DOM (`<pre><code><span style>`) and the color/style mapping 
are kept.
    - Carriage returns are now handled only by the caller's 
`checkAndReplaceCarriageReturn()`, removing the duplicate 
`escapeCarriageReturn()` pass.
    - All active decorations are applied (e.g. bold + underline + 
strikethrough), where `ansi-to-react` applied only the last one.
    - `ansi-to-react`'s Jupyter-Classic `\b` handling is dropped, matching the 
Angular `renderText()` (ansi_up), which does not do it either.
    
    ### What type of PR is it?
    Improvement
    
    ### Todos
    * [x] - Render ANSI output with `anser` directly in `TextRenderer.tsx`
    * [x] - Remove `ansi-to-react` and the `linkify-it` override
    * [x] - Add `TextRenderer.spec.tsx`
    
    ### What is the Jira issue?
    https://issues.apache.org/jira/browse/ZEPPELIN-6500
    
    ### How should this be tested?
    * `cd zeppelin-web-angular/projects/zeppelin-react && npm ci && npm test` 
passes, including the new `TextRenderer.spec.tsx`.
    * `npm ls linkify-it` is empty.
    * `npm audit --audit-level=high` no longer reports `linkify-it`. The 
remaining high findings (`braces` via `webpack-dev-server`/`ts-loader`) are 
dev-only and pre-existing on master.
    
    ### Screenshots (if appropriate)
    N/A
    
    ### Questions:
    * Does the license files need to update? No.
    * Is there breaking changes for older versions? No.
    * Does this needs documentation? No.
    
    Closes #5536 from dani1552/ZEPPELIN-6500.
    
    Signed-off-by: ChanHo Lee <[email protected]>
---
 .../projects/zeppelin-react/package-lock.json      | 48 +-------------------
 .../projects/zeppelin-react/package.json           |  3 +-
 .../src/components/renderers/TextRenderer.spec.tsx | 47 +++++++++++++++++++
 .../src/components/renderers/TextRenderer.tsx      | 53 +++++++++++++++++++++-
 4 files changed, 100 insertions(+), 51 deletions(-)

diff --git a/zeppelin-web-angular/projects/zeppelin-react/package-lock.json 
b/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
index 913ca4d249..870cbe0135 100644
--- a/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
+++ b/zeppelin-web-angular/projects/zeppelin-react/package-lock.json
@@ -11,7 +11,7 @@
         "@ant-design/icons": "5.4.0",
         "@zeppelin/notebook-core": "file:../zeppelin-notebook-core",
         "@zeppelin/sdk": "file:../zeppelin-sdk",
-        "ansi-to-react": "6.2.6",
+        "anser": "2.3.5",
         "antd": "5.21.0",
         "chart.js": "^4.5.1",
         "date-fns": "^3.6.0",
@@ -2894,21 +2894,6 @@
         "url": "https://github.com/chalk/ansi-styles?sponsor=1";
       }
     },
-    "node_modules/ansi-to-react": {
-      "version": "6.2.6",
-      "resolved": 
"https://registry.npmjs.org/ansi-to-react/-/ansi-to-react-6.2.6.tgz";,
-      "integrity": 
"sha512-Eqi0iaMK5OZ3jsVFxWvU2B74UZBnGuHlkflKMX6wTOeH+luy9KE2O0gUkc2PxhIP1R4IO0xohv62UMFInQOSeg==",
-      "license": "BSD-3-Clause",
-      "dependencies": {
-        "anser": "^2.3.2",
-        "escape-carriage": "^1.3.1",
-        "linkify-it": "^3.0.3"
-      },
-      "peerDependencies": {
-        "react": "^16.3.2 || ^17.0.0 || ^18.0.0 || ^19.0.0",
-        "react-dom": "^16.3.2 || ^17.0.0 || ^18.0.0 || ^19.0.0"
-      }
-    },
     "node_modules/antd": {
       "version": "5.21.0",
       "resolved": "https://registry.npmjs.org/antd/-/antd-5.21.0.tgz";,
@@ -4556,12 +4541,6 @@
         "node": ">=6"
       }
     },
-    "node_modules/escape-carriage": {
-      "version": "1.3.1",
-      "resolved": 
"https://registry.npmjs.org/escape-carriage/-/escape-carriage-1.3.1.tgz";,
-      "integrity": 
"sha512-GwBr6yViW3ttx1kb7/Oh+gKQ1/TrhYwxKqVmg5gS+BK+Qe2KrOa/Vh7w3HPBvgGf0LfcDGoY9I6NHKoA5Hozhw==",
-      "license": "MIT"
-    },
     "node_modules/escape-html": {
       "version": "1.0.3",
       "resolved": 
"https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz";,
@@ -6844,25 +6823,6 @@
         "url": "https://opencollective.com/parcel";
       }
     },
-    "node_modules/linkify-it": {
-      "version": "5.0.2",
-      "resolved": 
"https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz";,
-      "integrity": 
"sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==",
-      "funding": [
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/puzrin";
-        },
-        {
-          "type": "github",
-          "url": "https://github.com/sponsors/markdown-it";
-        }
-      ],
-      "license": "MIT",
-      "dependencies": {
-        "uc.micro": "^2.0.0"
-      }
-    },
     "node_modules/loader-runner": {
       "version": "4.3.2",
       "resolved": 
"https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.2.tgz";,
@@ -10308,12 +10268,6 @@
         "typescript": ">=4.8.4 <6.1.0"
       }
     },
-    "node_modules/uc.micro": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz";,
-      "integrity": 
"sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==",
-      "license": "MIT"
-    },
     "node_modules/unbox-primitive": {
       "version": "1.1.0",
       "resolved": 
"https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz";,
diff --git a/zeppelin-web-angular/projects/zeppelin-react/package.json 
b/zeppelin-web-angular/projects/zeppelin-react/package.json
index 6905d5001d..b9c5edc537 100644
--- a/zeppelin-web-angular/projects/zeppelin-react/package.json
+++ b/zeppelin-web-angular/projects/zeppelin-react/package.json
@@ -17,7 +17,7 @@
     "@ant-design/icons": "5.4.0",
     "@zeppelin/notebook-core": "file:../zeppelin-notebook-core",
     "@zeppelin/sdk": "file:../zeppelin-sdk",
-    "ansi-to-react": "6.2.6",
+    "anser": "2.3.5",
     "highlight.js": "^10.7.3",
     "antd": "5.21.0",
     "chart.js": "^4.5.1",
@@ -58,7 +58,6 @@
     "webpack-dev-server": "6.0.0"
   },
   "overrides": {
-    "linkify-it": "^5.0.2",
     "minimatch": "^10.2.4",
     "brace-expansion": "^5.0.8"
   }
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/TextRenderer.spec.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/TextRenderer.spec.tsx
new file mode 100644
index 0000000000..cdf3c38610
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/TextRenderer.spec.tsx
@@ -0,0 +1,47 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { render, screen } from '@testing-library/react';
+import { describe, expect, it } from 'vitest';
+import { TextRenderer } from './TextRenderer';
+
+describe('TextRenderer', () => {
+  it('renders plain text unchanged', () => {
+    const { container } = render(<TextRenderer text={'line one\nline two'} />);
+
+    expect(container.querySelector('code')!.textContent).toBe('line one\nline 
two');
+  });
+
+  it('strips ANSI escapes and colors the text they wrap', () => {
+    const { container } = render(<TextRenderer text={'\x1b[31mred\x1b[0m 
plain'} />);
+
+    expect(container.querySelector('code')!.textContent).toBe('red plain');
+    expect(screen.getByText('red').style.color).toBe('rgb(187, 0, 0)');
+    expect(screen.getByText('plain').style.color).toBe('');
+  });
+
+  it('applies background color and every active decoration', () => {
+    render(<TextRenderer text={'\x1b[1;4;9;42mstyled\x1b[0m'} />);
+
+    const span = screen.getByText('styled');
+    expect(span.style.backgroundColor).toBe('rgb(0, 187, 0)');
+    expect(span.style.fontWeight).toBe('bold');
+    expect(span.style.textDecoration).toBe('underline line-through');
+  });
+
+  it('leaves markup in the text literal', () => {
+    const { container } = render(<TextRenderer text={'<b>not bold</b>'} />);
+
+    expect(container.querySelector('b')).toBeNull();
+    expect(screen.getByText('<b>not bold</b>')).toBeTruthy();
+  });
+});
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/TextRenderer.tsx
 
b/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/TextRenderer.tsx
index 5b44a9704a..bbafad7b91 100644
--- 
a/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/TextRenderer.tsx
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/components/renderers/TextRenderer.tsx
@@ -10,17 +10,66 @@
  * limitations under the License.
  */
 
-import Ansi from 'ansi-to-react';
+import Anser from 'anser';
+import { CSSProperties } from 'react';
 
 export interface TextRendererProps {
   text: string;
 }
 
+const toStyle = ({ fg, bg, decorations }: Anser.AnserJsonEntry): CSSProperties 
=> {
+  const style: CSSProperties = {};
+  const textDecorations: string[] = [];
+  if (fg) {
+    style.color = `rgb(${fg})`;
+  }
+  if (bg) {
+    style.backgroundColor = `rgb(${bg})`;
+  }
+  decorations.forEach(decoration => {
+    switch (decoration) {
+      case 'bold':
+        style.fontWeight = 'bold';
+        break;
+      case 'dim':
+        style.opacity = 0.5;
+        break;
+      case 'italic':
+        style.fontStyle = 'italic';
+        break;
+      case 'hidden':
+        style.visibility = 'hidden';
+        break;
+      case 'underline':
+        textDecorations.push('underline');
+        break;
+      case 'strikethrough':
+        textDecorations.push('line-through');
+        break;
+      case 'blink':
+        textDecorations.push('blink');
+        break;
+    }
+  });
+  if (textDecorations.length > 0) {
+    style.textDecoration = textDecorations.join(' ');
+  }
+  return style;
+};
+
 // Matches Angular: result.component.ts renderText()
+// Carriage returns are resolved by the caller (checkAndReplaceCarriageReturn).
 export const TextRenderer = ({ text }: TextRendererProps) => {
+  const entries = Anser.ansiToJson(text, { json: true, remove_empty: true });
   return (
     <pre style={{ whiteSpace: 'pre-wrap', margin: 0 }}>
-      <Ansi>{text}</Ansi>
+      <code>
+        {entries.map((entry, i) => (
+          <span key={i} style={toStyle(entry)}>
+            {entry.content}
+          </span>
+        ))}
+      </code>
     </pre>
   );
 };

Reply via email to