Skip to content

Commit

Permalink
Merge pull request #184 from aodn/feature/5951-decode-html-entities
Browse files Browse the repository at this point in the history
Feature/5951 decode html entities
  • Loading branch information
NekoLyn authored Oct 4, 2024
2 parents 31afb34 + 0baa902 commit de95623
Show file tree
Hide file tree
Showing 7 changed files with 155 additions and 20 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -41,6 +42,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",
Expand All @@ -67,6 +69,7 @@
"@testing-library/jest-dom": "^6.5.0",
"@testing-library/react": "^14.3.1",
"@testing-library/user-event": "^14.5.2",
"@types/he": "^1",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.18",
"@typescript-eslint/eslint-plugin": "^7.18.0",
Expand Down
13 changes: 8 additions & 5 deletions src/components/list/listItem/subitem/ExpandableTextArea.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,16 +19,15 @@ const ExpandableTextArea: React.FC<ExpandableTextAreaProps> = ({
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 (
<TextAreaBaseGrid>
<Grid item md={12}>
Expand All @@ -41,7 +44,7 @@ const ExpandableTextArea: React.FC<ExpandableTextAreaProps> = ({
}}
onClick={onClick}
>
{isExpanded ? text : truncatedText}
{isExpanded ? decodedText : truncatedText}
</Typography>
</Grid>
<Grid item md={12} display="flex" justifyContent="flex-end">
Expand Down
5 changes: 4 additions & 1 deletion src/components/list/listItem/subitem/TextArea.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -9,7 +10,9 @@ interface TextAreaProps {
const TextArea: React.FC<TextAreaProps> = ({ text }) => {
return (
<TextAreaBaseGrid>
<Typography variant="detailContent">{text}</Typography>
<Typography variant="detailContent">
{decodeHtmlEntities(text)}
</Typography>
</TextAreaBaseGrid>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -154,7 +155,7 @@ const AbstractAndDownloadPanel: FC = () => {
<Grid item xs={12}>
<Stack direction="column">
<Typography sx={{ padding: 0 }} data-testid="detail-abstract">
{abstract}
{decodeHtmlEntities(abstract)}
</Typography>
<Box sx={{ visibility: "visible" }}>
<Box
Expand Down
29 changes: 16 additions & 13 deletions src/utils/StringUtils.tsx → src/utils/StringUtils.ts
Original file line number Diff line number Diff line change
@@ -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.
*
Expand All @@ -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;
}
};
104 changes: 104 additions & 0 deletions src/utils/__test__/StringUtils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import {
capitalizeFirstLetter,
decodeHtmlEntities,
formatNumber,
truncateText,
} from "../StringUtils";

describe("decodeHtmlEntities", () => {
it("should decode basic HTML entities", () => {
const input = "foo&amp;bar";
const output = decodeHtmlEntities(input);
expect(output).toBe("foo&bar");
});

it("shouldn't decode invalid HTML entities in strict mode", () => {
const input = "foo&ampbar";
const output = decodeHtmlEntities(input);
expect(output).toBe("foo&ampbar");
});

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");
});
});
18 changes: 18 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3878,6 +3878,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"
Expand Down Expand Up @@ -4774,6 +4781,7 @@ __metadata:
"@testing-library/react": "npm:^14.3.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"
Expand Down Expand Up @@ -4805,6 +4813,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"
Expand Down Expand Up @@ -7774,6 +7783,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"
Expand Down

0 comments on commit de95623

Please sign in to comment.