From 2cf5d628c4fadedf3320d62cb80fa2d72b8f5d91 Mon Sep 17 00:00:00 2001 From: HuaizhiDai Date: Thu, 3 Oct 2024 11:28:09 +1000 Subject: [PATCH 1/4] add new package to decode html entities --- package.json | 2 ++ yarn.lock | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/package.json b/package.json index bad6368f..916d9cec 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "dayjs": "^1.11.10", "events": "3.3.0", "geojson": "0.5.0", + "he": "^1.2.0", "http-proxy-middleware": "^2.0.6", "lodash": "^4.17.21", "mapbox-gl": "3.4.0", @@ -66,6 +67,7 @@ "@commitlint/cli": "^18.6.1", "@commitlint/config-conventional": "^18.6.2", "@emotion/react": "^11.11.3", + "@types/he": "^1", "@types/react": "^18.2.55", "@types/react-dom": "^18.2.18", "@typescript-eslint/eslint-plugin": "^7.18.0", diff --git a/yarn.lock b/yarn.lock index eac36950..5dc7c0e5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3671,6 +3671,13 @@ __metadata: languageName: node linkType: hard +"@types/he@npm:^1": + version: 1.2.3 + resolution: "@types/he@npm:1.2.3" + checksum: 10c0/562e4ec00e31e3d464e79e6da4b8a5c21999d38ceca6a8facaa96e89c2d646f410bb58bb81f48a7472aeb4655ce40d27b7d77e5a4fa5a2d9caa0f3037caab5b7 + languageName: node + linkType: hard + "@types/http-proxy@npm:^1.17.8": version: 1.17.14 resolution: "@types/http-proxy@npm:1.17.14" @@ -4517,6 +4524,7 @@ __metadata: "@testing-library/react": "npm:^14.2.1" "@testing-library/user-event": "npm:^14.5.2" "@turf/turf": "npm:7.0.0" + "@types/he": "npm:^1" "@types/jest": "npm:^29.5.12" "@types/lodash": "npm:^4.17.0" "@types/mapbox__geo-viewport": "npm:^0.5.3" @@ -4548,6 +4556,7 @@ __metadata: eslint-plugin-react-refresh: "npm:^0.4.5" events: "npm:3.3.0" geojson: "npm:0.5.0" + he: "npm:^1.2.0" http-proxy-middleware: "npm:^2.0.6" husky: "npm:^9.1.1" lint-staged: "npm:^15.2.2" @@ -7511,6 +7520,15 @@ __metadata: languageName: node linkType: hard +"he@npm:^1.2.0": + version: 1.2.0 + resolution: "he@npm:1.2.0" + bin: + he: bin/he + checksum: 10c0/a27d478befe3c8192f006cdd0639a66798979dfa6e2125c6ac582a19a5ebfec62ad83e8382e6036170d873f46e4536a7e795bf8b95bf7c247f4cc0825ccc8c17 + languageName: node + linkType: hard + "headers-polyfill@npm:^4.0.2": version: 4.0.3 resolution: "headers-polyfill@npm:4.0.3" From 470027e61132072c8825fa5e75bbaaa1551ba415 Mon Sep 17 00:00:00 2001 From: HuaizhiDai Date: Thu, 3 Oct 2024 11:28:51 +1000 Subject: [PATCH 2/4] add new script to run app without tests --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 916d9cec..cd5c002d 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "bash generateEnv.sh dev && vitest run && vite --mode dev", + "dev:no-test": "bash generateEnv.sh dev && vite --mode dev", "edge": "bash generateEnv.sh edge && tsc && vitest run && vite build --mode edge", "staging": "bash generateEnv.sh staging && tsc && vitest run && vite build --mode staging", "prod": "bash generateEnv.sh prod && tsc && vitest run && vite build --mode prod", From f1ff6dff37f273d426513d33fdafcab12aa53299 Mon Sep 17 00:00:00 2001 From: HuaizhiDai Date: Thu, 3 Oct 2024 11:30:39 +1000 Subject: [PATCH 3/4] new string utils and corresponding tests --- src/utils/{StringUtils.tsx => StringUtils.ts} | 29 ++--- src/utils/__test__/StringUtils.test.ts | 104 ++++++++++++++++++ 2 files changed, 120 insertions(+), 13 deletions(-) rename src/utils/{StringUtils.tsx => StringUtils.ts} (54%) create mode 100644 src/utils/__test__/StringUtils.test.ts diff --git a/src/utils/StringUtils.tsx b/src/utils/StringUtils.ts similarity index 54% rename from src/utils/StringUtils.tsx rename to src/utils/StringUtils.ts index 5074d625..4d824373 100644 --- a/src/utils/StringUtils.tsx +++ b/src/utils/StringUtils.ts @@ -1,6 +1,8 @@ // This file is only for string manipulation related helper methods // e.g capitalize, concatenate, ... +import { decode } from "he"; + /** * Capitalizes the first letter of a given string. * @@ -23,23 +25,24 @@ export const capitalizeFirstLetter = (str: string): string => * @param {number} number - The number to format. * @returns {string} The formatted number as a string. */ -export const formatNumber = (number: number) => { +export const formatNumber = (number: number): string => { return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); }; /** - * Trims a string to a specified length and adds an ellipsis if necessary. - * @param c - The input string to trim. Can be undefined. - * @param size - The maximum length of the trimmed string. Defaults to 90. - * @returns The trimmed string, or an empty string if the input is undefined. + * Truncates a string to a specified length. If the string is not longer than the specified length, the original string is returned. + * @param {string} str - The string to truncate. + * @param {number} truncateCount - The number of characters to truncate the string to. + * @returns {string} The truncated string. */ -export const trimContent = ( - c: string | undefined, - size: number = 90 -): string => { - if (c) { - return `${c.slice(0, 90)}${c.length > size ? "..." : ""}`; - } else { - return ""; +export const truncateText = (str: string, truncateCount: number): string => { + return str.length > truncateCount ? str.slice(0, truncateCount) + "..." : str; +}; + +export const decodeHtmlEntities = (str: string): string => { + try { + return decode(str, { strict: true }); + } catch (ignored) { + return str; } }; diff --git a/src/utils/__test__/StringUtils.test.ts b/src/utils/__test__/StringUtils.test.ts new file mode 100644 index 00000000..63111b63 --- /dev/null +++ b/src/utils/__test__/StringUtils.test.ts @@ -0,0 +1,104 @@ +import { + capitalizeFirstLetter, + decodeHtmlEntities, + formatNumber, + truncateText, +} from "../StringUtils"; + +describe("decodeHtmlEntities", () => { + it("should decode basic HTML entities", () => { + const input = "foo&bar"; + const output = decodeHtmlEntities(input); + expect(output).toBe("foo&bar"); + }); + + it("shouldn't decode invalid HTML entities in strict mode", () => { + const input = "foo&bar"; + const output = decodeHtmlEntities(input); + expect(output).toBe("foo&bar"); + }); + + it("should handle invalid HTML entities gracefully", () => { + const input = "foo&invalid;bar"; + const output = decodeHtmlEntities(input); + expect(output).toBe("foo&invalid;bar"); + }); +}); + +describe("truncateText", () => { + it("should truncate a string longer than the specified length", () => { + const input = "Hello, World!"; + const output = truncateText(input, 5); + expect(output).toBe("Hello..."); + }); + + it("should return the original string if it is shorter than the specified length", () => { + const input = "Hello"; + const output = truncateText(input, 10); + expect(output).toBe("Hello"); + }); + + it("should handle an empty string", () => { + const input = ""; + const output = truncateText(input, 5); + expect(output).toBe(""); + }); + + it("should return the original string if it is exactly the specified length", () => { + const input = "Hello"; + const output = truncateText(input, 5); + expect(output).toBe("Hello"); + }); +}); + +describe("capitalizeFirstLetter", () => { + it("should capitalize the first letter of a lowercase string", () => { + const input = "hello"; + const output = capitalizeFirstLetter(input); + expect(output).toBe("Hello"); + }); + + it("should capitalize the first letter of an already capitalized string", () => { + const input = "Hello"; + const output = capitalizeFirstLetter(input); + expect(output).toBe("Hello"); + }); + + it("should handle an empty string", () => { + const input = ""; + const output = capitalizeFirstLetter(input); + expect(output).toBe(""); + }); + + it("should capitalize the first letter of a string with mixed case", () => { + const input = "hElLo"; + const output = capitalizeFirstLetter(input); + expect(output).toBe("Hello"); + }); +}); + +describe("formatNumber", () => { + it("should format a number with thousands separators", () => { + const input = 1234567; + const output = formatNumber(input); + expect(output).toBe("1,234,567"); + }); + + it("should handle a number without thousands separators", () => { + const input = 123; + const output = formatNumber(input); + expect(output).toBe("123"); + }); + + it("should handle zero", () => { + const input = 0; + const output = formatNumber(input); + expect(output).toBe("0"); + }); + + it("should handle negative numbers", () => { + const input = -1234567; + const output = formatNumber(input); + expect(output).toBe("-1,234,567"); + }); +}); From 0b2855d3dac383467aabb4bbf69fabc7d7de6315 Mon Sep 17 00:00:00 2001 From: HuaizhiDai Date: Thu, 3 Oct 2024 14:01:53 +1000 Subject: [PATCH 4/4] implement html entities decoding --- .../list/listItem/subitem/ExpandableTextArea.tsx | 13 ++++++++----- src/components/list/listItem/subitem/TextArea.tsx | 5 ++++- .../tab-panels/AbstractAndDownloadPanel.tsx | 3 ++- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/components/list/listItem/subitem/ExpandableTextArea.tsx b/src/components/list/listItem/subitem/ExpandableTextArea.tsx index c1f965a4..afaa0920 100644 --- a/src/components/list/listItem/subitem/ExpandableTextArea.tsx +++ b/src/components/list/listItem/subitem/ExpandableTextArea.tsx @@ -1,6 +1,10 @@ import React, { useCallback, useState } from "react"; import TextAreaBaseGrid from "./TextAreaBaseGrid"; import { Button, Grid, Typography } from "@mui/material"; +import { + decodeHtmlEntities, + truncateText, +} from "../../../../utils/StringUtils"; interface ExpandableTextAreaProps { text: string; @@ -15,16 +19,15 @@ const ExpandableTextArea: React.FC = ({ isClickable = false, onClick = () => {}, }) => { - const truncatedText = - text.length > truncateCount ? text.slice(0, truncateCount) + "..." : text; - const doesNeedTruncation = text.length > truncateCount; + const decodedText = decodeHtmlEntities(text); + const truncatedText = truncateText(decodedText, truncateCount); + const doesNeedTruncation = decodedText.length > truncateCount; const [isExpanded, setIsExpanded] = useState(false); const onButtonClick = useCallback(() => { setIsExpanded((prev) => !prev); }, []); - return ( @@ -41,7 +44,7 @@ const ExpandableTextArea: React.FC = ({ }} onClick={onClick} > - {isExpanded ? text : truncatedText} + {isExpanded ? decodedText : truncatedText} diff --git a/src/components/list/listItem/subitem/TextArea.tsx b/src/components/list/listItem/subitem/TextArea.tsx index 1f3fb243..bfe03af1 100644 --- a/src/components/list/listItem/subitem/TextArea.tsx +++ b/src/components/list/listItem/subitem/TextArea.tsx @@ -1,6 +1,7 @@ import React from "react"; import { Typography } from "@mui/material"; import TextAreaBaseGrid from "./TextAreaBaseGrid"; +import { decodeHtmlEntities } from "../../../../utils/StringUtils"; interface TextAreaProps { text: string; @@ -9,7 +10,9 @@ interface TextAreaProps { const TextArea: React.FC = ({ text }) => { return ( - {text} + + {decodeHtmlEntities(text)} + ); }; diff --git a/src/pages/detail-page/subpages/tab-panels/AbstractAndDownloadPanel.tsx b/src/pages/detail-page/subpages/tab-panels/AbstractAndDownloadPanel.tsx index 3f002a66..8127fbb0 100644 --- a/src/pages/detail-page/subpages/tab-panels/AbstractAndDownloadPanel.tsx +++ b/src/pages/detail-page/subpages/tab-panels/AbstractAndDownloadPanel.tsx @@ -43,6 +43,7 @@ import GeojsonLayer from "../../../../components/map/mapbox/layers/GeojsonLayer" import { StaticLayersDef } from "../../../../components/map/mapbox/layers/StaticLayer"; import { MapboxWorldLayersDef } from "../../../../components/map/mapbox/layers/MapboxWorldLayer"; import useScrollToSection from "../../../../hooks/useScrollToSection"; +import { decodeHtmlEntities } from "../../../../utils/StringUtils"; interface DownloadSelect { label?: string; @@ -154,7 +155,7 @@ const AbstractAndDownloadPanel: FC = () => { - {abstract} + {decodeHtmlEntities(abstract)}