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>
);
};