@tapcart/mobile-components 0.11.5 → 0.11.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/hooks/use-infinite-wishlist.d.ts.map +1 -1
- package/dist/components/hooks/use-infinite-wishlist.js +15 -2
- package/dist/components/ui/image.d.ts.map +1 -1
- package/dist/components/ui/image.js +2 -1
- package/dist/components/ui/money.d.ts.map +1 -1
- package/dist/components/ui/money.js +53 -21
- package/dist/components/ui/money.test.d.ts +2 -0
- package/dist/components/ui/money.test.d.ts.map +1 -0
- package/dist/components/ui/money.test.js +57 -0
- package/dist/lib/utils/html.d.ts +16 -0
- package/dist/lib/utils/html.d.ts.map +1 -0
- package/dist/lib/utils/html.js +56 -0
- package/dist/styles.css +7 -0
- package/dist/tests/utils/html.test.d.ts +2 -0
- package/dist/tests/utils/html.test.d.ts.map +1 -0
- package/dist/tests/utils/html.test.js +104 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-infinite-wishlist.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-infinite-wishlist.ts"],"names":[],"mappings":"AAOA,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,KAAK,0BAA0B,GAAG;IAChC,iBAAiB,EAAE,MAAM,EAAE,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,OAAO,CAAA;IACnB,cAAc,CAAC,EAAE,cAAc,CAAA;IAC/B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CACtB,CAAA;AAED,KAAK,cAAc,GAAG;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE;QACL,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;IACD,cAAc,EAAE;QACd,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;IACD,gBAAgB,EAAE,OAAO,CAAA;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,eAAe,EAAE,KAAK,CAAC;QACrB,IAAI,EAAE,MAAM,CAAA;QACZ,KAAK,EAAE,MAAM,CAAA;KACd,CAAC,CAAA;IACF,KAAK,CAAC,EAAE;QACN,GAAG,EAAE,MAAM,CAAA;QACX,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;CACF,CAAA;AAED,KAAK,OAAO,GAAG;IACb,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,gBAAgB,EAAE,OAAO,CAAA;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,aAAa,CAAC,EAAE;QACd,GAAG,EAAE,MAAM,CAAA;QACX,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;CACF,CAAA;AAKD,wBAAgB,mBAAmB,CAAC,EAClC,iBAAiB,EACjB,SAAS,EACT,MAAM,EACN,UAAkB,EAClB,cAAmB,EACnB,UAAe,GAChB,EAAE,0BAA0B;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"use-infinite-wishlist.d.ts","sourceRoot":"","sources":["../../../components/hooks/use-infinite-wishlist.ts"],"names":[],"mappings":"AAOA,KAAK,cAAc,GAAG;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,KAAK,0BAA0B,GAAG;IAChC,iBAAiB,EAAE,MAAM,EAAE,CAAA;IAC3B,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,MAAM,CAAA;IACd,UAAU,EAAE,OAAO,CAAA;IACnB,cAAc,CAAC,EAAE,cAAc,CAAA;IAC/B,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;CACtB,CAAA;AAED,KAAK,cAAc,GAAG;IACpB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,KAAK,EAAE;QACL,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;IACD,cAAc,EAAE;QACd,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;IACD,gBAAgB,EAAE,OAAO,CAAA;IACzB,GAAG,EAAE,MAAM,CAAA;IACX,eAAe,EAAE,KAAK,CAAC;QACrB,IAAI,EAAE,MAAM,CAAA;QACZ,KAAK,EAAE,MAAM,CAAA;KACd,CAAC,CAAA;IACF,KAAK,CAAC,EAAE;QACN,GAAG,EAAE,MAAM,CAAA;QACX,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;CACF,CAAA;AAED,KAAK,OAAO,GAAG;IACb,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,WAAW,EAAE,MAAM,CAAA;IACnB,gBAAgB,EAAE,OAAO,CAAA;IACzB,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,EAAE,cAAc,EAAE,CAAA;IAC1B,aAAa,CAAC,EAAE;QACd,GAAG,EAAE,MAAM,CAAA;QACX,OAAO,EAAE,MAAM,CAAA;QACf,KAAK,EAAE,MAAM,CAAA;QACb,MAAM,EAAE,MAAM,CAAA;KACf,CAAA;CACF,CAAA;AAKD,wBAAgB,mBAAmB,CAAC,EAClC,iBAAiB,EACjB,SAAS,EACT,MAAM,EACN,UAAkB,EAClB,cAAmB,EACnB,UAAe,GAChB,EAAE,0BAA0B;;;;;;;;;EAsO5B"}
|
|
@@ -77,7 +77,13 @@ export function useInfiniteWishlist({ initialProductIds, chunkSize, apiUrl, shou
|
|
|
77
77
|
handleFetchError(err, "fetch mocked products");
|
|
78
78
|
return [];
|
|
79
79
|
}
|
|
80
|
-
}), [
|
|
80
|
+
}), [
|
|
81
|
+
apiUrl,
|
|
82
|
+
collectionToFetchID,
|
|
83
|
+
queryVariables === null || queryVariables === void 0 ? void 0 : queryVariables.language,
|
|
84
|
+
queryVariables === null || queryVariables === void 0 ? void 0 : queryVariables.country,
|
|
85
|
+
queryVariables === null || queryVariables === void 0 ? void 0 : queryVariables.appId,
|
|
86
|
+
]);
|
|
81
87
|
const fetchProducts = useCallback((ids) => __awaiter(this, void 0, void 0, function* () {
|
|
82
88
|
if (!ids.length)
|
|
83
89
|
return [];
|
|
@@ -85,6 +91,13 @@ export function useInfiniteWishlist({ initialProductIds, chunkSize, apiUrl, shou
|
|
|
85
91
|
return fetchMockedProducts();
|
|
86
92
|
const queryParams = new URLSearchParams();
|
|
87
93
|
queryParams.set("ids", getProductGidsFromIds(ids).join(","));
|
|
94
|
+
// Add country and language parameters for correct currency/pricing
|
|
95
|
+
if (queryVariables === null || queryVariables === void 0 ? void 0 : queryVariables.country) {
|
|
96
|
+
queryParams.set("country", queryVariables.country);
|
|
97
|
+
}
|
|
98
|
+
if (queryVariables === null || queryVariables === void 0 ? void 0 : queryVariables.language) {
|
|
99
|
+
queryParams.set("language", queryVariables.language);
|
|
100
|
+
}
|
|
88
101
|
const url = `${apiUrl}/products/by-ids?${queryParams.toString()}`;
|
|
89
102
|
try {
|
|
90
103
|
const response = yield fetch(url);
|
|
@@ -115,7 +128,7 @@ export function useInfiniteWishlist({ initialProductIds, chunkSize, apiUrl, shou
|
|
|
115
128
|
handleFetchError(err, "fetch products");
|
|
116
129
|
return [];
|
|
117
130
|
}
|
|
118
|
-
}), [apiUrl, shouldMock, fetchMockedProducts, variantIds]);
|
|
131
|
+
}), [apiUrl, shouldMock, fetchMockedProducts, variantIds, queryVariables]);
|
|
119
132
|
const loadMore = useCallback(() => __awaiter(this, void 0, void 0, function* () {
|
|
120
133
|
if (loadingRef.current ||
|
|
121
134
|
(!shouldMock && !(allProductIds.length - loadedIndex > 0))) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../../components/ui/image.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAE9B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAC5C,OAAO,KAAK,EAAE,KAAK,IAAI,SAAS,EAAE,MAAM,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"image.d.ts","sourceRoot":"","sources":["../../../components/ui/image.tsx"],"names":[],"mappings":"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"}
|
|
@@ -14,6 +14,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
14
14
|
/* eslint-disable @next/next/no-img-element */
|
|
15
15
|
import * as React from "react";
|
|
16
16
|
import ReactDOM from "react-dom";
|
|
17
|
+
import { stripHtml } from "../../lib/utils/html";
|
|
17
18
|
function ImagePreload({ imgAttributes }) {
|
|
18
19
|
const opts = {
|
|
19
20
|
as: "image",
|
|
@@ -102,7 +103,7 @@ export const Image = React.forwardRef((_a, ref) => {
|
|
|
102
103
|
: "";
|
|
103
104
|
const nHeight = autoHeight ? "auto" : fixedHeight;
|
|
104
105
|
const nSrc = src || (data === null || data === void 0 ? void 0 : data.url);
|
|
105
|
-
const nAlt = (data === null || data === void 0 ? void 0 : data.altText) && !alt ? data === null || data === void 0 ? void 0 : data.altText : alt || "";
|
|
106
|
+
const nAlt = stripHtml((data === null || data === void 0 ? void 0 : data.altText) && !alt ? data === null || data === void 0 ? void 0 : data.altText : alt || "");
|
|
106
107
|
const nAspectRatio = aspectRatio
|
|
107
108
|
? aspectRatio
|
|
108
109
|
: normalizedData.unitsMatch
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../../../components/ui/money.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAGjE,UAAU,SAAS;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;CACd;AAED,QAAA,MAAM,aAAa,gGAMjB,CAAA;AAEF,MAAM,WAAW,UACf,SAAQ,SAAS,EACf,YAAY,CAAC,OAAO,aAAa,CAAC;IACpC,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,MAAM,CAAC,EAAE,KAAK,CAAC,aAAa,CAAA;CAC7B;AAED,iBAAS,KAAK,CAAC,EACb,KAAK,EACL,MAAM,EACN,QAAQ,EACR,aAAqB,EACrB,MAAM,EACN,GAAG,KAAK,EACT,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"money.d.ts","sourceRoot":"","sources":["../../../components/ui/money.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAA;AAC9B,OAAO,EAAO,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAA;AAGjE,UAAU,SAAS;IACjB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;CACd;AAED,QAAA,MAAM,aAAa,gGAMjB,CAAA;AAEF,MAAM,WAAW,UACf,SAAQ,SAAS,EACf,YAAY,CAAC,OAAO,aAAa,CAAC;IACpC,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,MAAM,CAAC,EAAE,KAAK,CAAC,aAAa,CAAA;CAC7B;AAED,iBAAS,KAAK,CAAC,EACb,KAAK,EACL,MAAM,EACN,QAAQ,EACR,aAAqB,EACrB,MAAM,EACN,GAAG,KAAK,EACT,EAAE,UAAU,2CAiFZ;AAED,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,CAAA"}
|
|
@@ -25,27 +25,59 @@ function Money(_a) {
|
|
|
25
25
|
var _b, _c;
|
|
26
26
|
var { price, locale, currency, hideZeroCents = false, styles } = _a, props = __rest(_a, ["price", "locale", "currency", "hideZeroCents", "styles"]);
|
|
27
27
|
const searchParams = useSearchParams();
|
|
28
|
-
const countryFromParams = searchParams === null || searchParams === void 0 ? void 0 : searchParams.get("country");
|
|
29
|
-
//
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
28
|
+
const countryFromParams = (searchParams === null || searchParams === void 0 ? void 0 : searchParams.get("country")) || undefined;
|
|
29
|
+
// Derive a candidate locale from the route or prop, then normalize to a valid BCP 47 tag
|
|
30
|
+
const routeCandidate = ((_c = (_b = usePathname()) === null || _b === void 0 ? void 0 : _b.split("/")) === null || _c === void 0 ? void 0 : _c.pop()) || "";
|
|
31
|
+
const normalizeLocale = (baseLocale, countryParam) => {
|
|
32
|
+
const DEFAULT_LOCALE = "en-US";
|
|
33
|
+
const raw = (baseLocale || "").replace(/_/g, "-").trim();
|
|
34
|
+
let languageCode = "";
|
|
35
|
+
let regionCode = "";
|
|
36
|
+
if (raw.includes("-")) {
|
|
37
|
+
const [langPart, regionPart] = raw.split("-");
|
|
38
|
+
if (langPart)
|
|
39
|
+
languageCode = langPart.toLowerCase();
|
|
40
|
+
if (regionPart)
|
|
41
|
+
regionCode = regionPart.toUpperCase();
|
|
42
|
+
}
|
|
43
|
+
else if (raw.length === 2) {
|
|
44
|
+
if (/^[a-z]{2}$/.test(raw)) {
|
|
45
|
+
// language only (e.g., "en")
|
|
46
|
+
languageCode = raw.toLowerCase();
|
|
47
|
+
}
|
|
48
|
+
else if (/^[A-Z]{2}$/.test(raw)) {
|
|
49
|
+
// region only (e.g., "US")
|
|
50
|
+
regionCode = raw.toUpperCase();
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
languageCode = raw.toLowerCase();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else if (raw) {
|
|
57
|
+
// fallback: treat as language subtag
|
|
58
|
+
languageCode = raw.toLowerCase();
|
|
59
|
+
}
|
|
60
|
+
// If a country param is present, prefer it as region
|
|
61
|
+
if (countryParam) {
|
|
62
|
+
regionCode = countryParam.toUpperCase();
|
|
63
|
+
}
|
|
64
|
+
// Ensure we always have a sensible language
|
|
65
|
+
if (!languageCode) {
|
|
66
|
+
languageCode = "en";
|
|
67
|
+
}
|
|
68
|
+
const candidate = regionCode
|
|
69
|
+
? `${languageCode}-${regionCode}`
|
|
70
|
+
: languageCode;
|
|
71
|
+
try {
|
|
72
|
+
// Validate and canonicalize via NumberFormat
|
|
73
|
+
const formatter = new Intl.NumberFormat(candidate);
|
|
74
|
+
return formatter.resolvedOptions().locale;
|
|
75
|
+
}
|
|
76
|
+
catch (_a) {
|
|
77
|
+
return DEFAULT_LOCALE;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const language = normalizeLocale(routeCandidate || locale, countryFromParams);
|
|
49
81
|
const formatter = React.useMemo(() => new Intl.NumberFormat(language, {
|
|
50
82
|
style: "currency",
|
|
51
83
|
currency: currency,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"money.test.d.ts","sourceRoot":"","sources":["../../../components/ui/money.test.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { Money } from "./money";
|
|
4
|
+
jest.mock("next/navigation", () => ({
|
|
5
|
+
usePathname: jest.fn(),
|
|
6
|
+
useSearchParams: jest.fn(),
|
|
7
|
+
}));
|
|
8
|
+
const mockedUsePathname = require("next/navigation").usePathname;
|
|
9
|
+
const mockedUseSearchParams = require("next/navigation")
|
|
10
|
+
.useSearchParams;
|
|
11
|
+
function makeSearchParams(params) {
|
|
12
|
+
return {
|
|
13
|
+
get: (key) => params[key],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
describe("Money", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
mockedUsePathname.mockReset();
|
|
19
|
+
mockedUseSearchParams.mockReset();
|
|
20
|
+
});
|
|
21
|
+
it("formats USD with default en-US when no locale provided", () => {
|
|
22
|
+
mockedUsePathname.mockReturnValue("/");
|
|
23
|
+
mockedUseSearchParams.mockReturnValue(makeSearchParams({}));
|
|
24
|
+
render(_jsx(Money, { price: 1234.56, currency: "USD", locale: "" }));
|
|
25
|
+
expect(screen.getByText(/\$1,234.56/)).toBeTruthy();
|
|
26
|
+
});
|
|
27
|
+
it("removes trailing cents when hideZeroCents is true", () => {
|
|
28
|
+
mockedUsePathname.mockReturnValue("/");
|
|
29
|
+
mockedUseSearchParams.mockReturnValue(makeSearchParams({}));
|
|
30
|
+
render(_jsx(Money, { price: 100, currency: "USD", locale: "en-US", hideZeroCents: true }));
|
|
31
|
+
expect(screen.getByText("$100")).toBeTruthy();
|
|
32
|
+
});
|
|
33
|
+
it("uses route locale when valid (e.g., fr-FR)", () => {
|
|
34
|
+
mockedUsePathname.mockReturnValue("/shop/fr-FR");
|
|
35
|
+
mockedUseSearchParams.mockReturnValue(makeSearchParams({}));
|
|
36
|
+
render(_jsx(Money, { price: 1234.56, currency: "EUR", locale: "en-US" }));
|
|
37
|
+
expect(screen.getByText(/1\s?234,56\s?€/)).toBeTruthy();
|
|
38
|
+
});
|
|
39
|
+
it("normalizes underscore and applies country param for region", () => {
|
|
40
|
+
mockedUsePathname.mockReturnValue("/shop/es_es");
|
|
41
|
+
mockedUseSearchParams.mockReturnValue(makeSearchParams({ country: "mx" }));
|
|
42
|
+
render(_jsx(Money, { price: 50, currency: "MXN", locale: "es" }));
|
|
43
|
+
expect(screen.getByText(/\$\s?50(\.00)?/)).toBeTruthy();
|
|
44
|
+
});
|
|
45
|
+
it("avoids malformed tags like en-us-US and falls back safely", () => {
|
|
46
|
+
mockedUsePathname.mockReturnValue("/en-us-US");
|
|
47
|
+
mockedUseSearchParams.mockReturnValue(makeSearchParams({}));
|
|
48
|
+
render(_jsx(Money, { price: 10, currency: "USD", locale: "en-us-US" }));
|
|
49
|
+
expect(screen.getByText(/\$10(\.00)?/)).toBeTruthy();
|
|
50
|
+
});
|
|
51
|
+
it("handles region-only input via country param", () => {
|
|
52
|
+
mockedUsePathname.mockReturnValue("/US");
|
|
53
|
+
mockedUseSearchParams.mockReturnValue(makeSearchParams({ country: "US" }));
|
|
54
|
+
render(_jsx(Money, { price: 20, currency: "USD", locale: "" }));
|
|
55
|
+
expect(screen.getByText(/\$20(\.00)?/)).toBeTruthy();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strips HTML tags from a string using DOM parsing to handle nested elements correctly.
|
|
3
|
+
*
|
|
4
|
+
* @param html - The HTML string to strip tags from
|
|
5
|
+
* @returns Clean text content with HTML tags removed
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```
|
|
9
|
+
* stripHtml('<p><strong>New Arrivals</strong></p>') // returns 'New Arrivals'
|
|
10
|
+
* stripHtml('<div>Hello <span>World</span>!</div>') // returns 'Hello World!'
|
|
11
|
+
* stripHtml('<p>First</p><p>Second</p>') // returns 'First Second'
|
|
12
|
+
* stripHtml('Plain text') // returns 'Plain text'
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export declare const stripHtml: (html: string | null | undefined) => string;
|
|
16
|
+
//# sourceMappingURL=html.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../../lib/utils/html.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,SAAS,SAAU,MAAM,GAAG,IAAI,GAAG,SAAS,KAAG,MAgD3D,CAAA"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strips HTML tags from a string using DOM parsing to handle nested elements correctly.
|
|
3
|
+
*
|
|
4
|
+
* @param html - The HTML string to strip tags from
|
|
5
|
+
* @returns Clean text content with HTML tags removed
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```
|
|
9
|
+
* stripHtml('<p><strong>New Arrivals</strong></p>') // returns 'New Arrivals'
|
|
10
|
+
* stripHtml('<div>Hello <span>World</span>!</div>') // returns 'Hello World!'
|
|
11
|
+
* stripHtml('<p>First</p><p>Second</p>') // returns 'First Second'
|
|
12
|
+
* stripHtml('Plain text') // returns 'Plain text'
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export const stripHtml = (html) => {
|
|
16
|
+
// Handle null, undefined, or empty inputs
|
|
17
|
+
if (typeof html !== "string") {
|
|
18
|
+
return "";
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
// Create a temporary DOM element
|
|
22
|
+
const tempDiv = document.createElement("div");
|
|
23
|
+
// Set the HTML content
|
|
24
|
+
tempDiv.innerHTML = html;
|
|
25
|
+
// Add spaces between block-level elements for accessibility and screen reader compatibility
|
|
26
|
+
// This ensures proper separation between content blocks while preserving existing meaningful whitespace
|
|
27
|
+
const blockElements = tempDiv.querySelectorAll("p, div, h1, h2, h3, h4, h5, h6, li, blockquote, pre, section, article, aside, header, footer, main, nav");
|
|
28
|
+
blockElements.forEach((element, index) => {
|
|
29
|
+
var _a;
|
|
30
|
+
// Skip the first element (index 0) to avoid adding a leading space
|
|
31
|
+
if (index === 0)
|
|
32
|
+
return;
|
|
33
|
+
// Check if there's already meaningful whitespace before this element
|
|
34
|
+
const prevSibling = element.previousSibling;
|
|
35
|
+
const hasWhitespaceBefore = prevSibling &&
|
|
36
|
+
prevSibling.nodeType === Node.TEXT_NODE &&
|
|
37
|
+
/\s$/.test(prevSibling.textContent || "");
|
|
38
|
+
// If whitespace already exists, don't add another space
|
|
39
|
+
if (hasWhitespaceBefore)
|
|
40
|
+
return;
|
|
41
|
+
// Only insert a space if there's no existing whitespace
|
|
42
|
+
// This prevents "FirstSecond" and ensures "First Second" for screen readers
|
|
43
|
+
const spaceNode = document.createTextNode(" ");
|
|
44
|
+
(_a = element.parentNode) === null || _a === void 0 ? void 0 : _a.insertBefore(spaceNode, element);
|
|
45
|
+
});
|
|
46
|
+
// Extract text content (this handles nested elements automatically)
|
|
47
|
+
const textContent = tempDiv.textContent || tempDiv.innerText || "";
|
|
48
|
+
// Return trimmed text
|
|
49
|
+
return textContent.trim();
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
// If DOM parsing fails for any reason, return the original string
|
|
53
|
+
console.warn("Failed to strip HTML, returning original string:", error);
|
|
54
|
+
return html;
|
|
55
|
+
}
|
|
56
|
+
};
|
package/dist/styles.css
CHANGED
|
@@ -841,6 +841,10 @@ video {
|
|
|
841
841
|
margin-top: 0px;
|
|
842
842
|
margin-bottom: 0px;
|
|
843
843
|
}
|
|
844
|
+
.my-1 {
|
|
845
|
+
margin-top: 0.25rem;
|
|
846
|
+
margin-bottom: 0.25rem;
|
|
847
|
+
}
|
|
844
848
|
.my-3 {
|
|
845
849
|
margin-top: 0.75rem;
|
|
846
850
|
margin-bottom: 0.75rem;
|
|
@@ -1679,6 +1683,9 @@ video {
|
|
|
1679
1683
|
.border-t-\[0px\] {
|
|
1680
1684
|
border-top-width: 0px;
|
|
1681
1685
|
}
|
|
1686
|
+
.border-solid {
|
|
1687
|
+
border-style: solid;
|
|
1688
|
+
}
|
|
1682
1689
|
.border-none {
|
|
1683
1690
|
border-style: none;
|
|
1684
1691
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"html.test.d.ts","sourceRoot":"","sources":["../../../tests/utils/html.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { stripHtml } from "../../lib/utils/html";
|
|
2
|
+
describe("stripHtml", () => {
|
|
3
|
+
describe("nested HTML handling", () => {
|
|
4
|
+
it("should strip nested HTML tags correctly", () => {
|
|
5
|
+
const result = stripHtml("<p><strong>New Arrivals</strong></p>");
|
|
6
|
+
expect(result).toBe("New Arrivals");
|
|
7
|
+
});
|
|
8
|
+
it("should handle multiple nested elements", () => {
|
|
9
|
+
const result = stripHtml("<div>Hello <span>World</span>!</div>");
|
|
10
|
+
expect(result).toBe("Hello World!");
|
|
11
|
+
});
|
|
12
|
+
it("should handle deeply nested HTML", () => {
|
|
13
|
+
const result = stripHtml("<div><p><span><strong>Deep</strong> <em>nested</em></span> text</p></div>");
|
|
14
|
+
expect(result).toBe("Deep nested text");
|
|
15
|
+
});
|
|
16
|
+
it("should handle multiple paragraph elements at the top level", () => {
|
|
17
|
+
const result = stripHtml("<p>First paragraph</p><p>Second paragraph</p><p>Third paragraph</p>");
|
|
18
|
+
expect(result).toBe("First paragraph Second paragraph Third paragraph");
|
|
19
|
+
});
|
|
20
|
+
it("should handle mixed block-level elements with spaces", () => {
|
|
21
|
+
const result = stripHtml("<h1>Title</h1><p>Content</p><div>Footer</div>");
|
|
22
|
+
expect(result).toBe("Title Content Footer");
|
|
23
|
+
});
|
|
24
|
+
it("should handle list items with spaces", () => {
|
|
25
|
+
const result = stripHtml("<li>Item 1</li><li>Item 2</li><li>Item 3</li>");
|
|
26
|
+
expect(result).toBe("Item 1 Item 2 Item 3");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe("plain text handling", () => {
|
|
30
|
+
it("should return plain text unchanged", () => {
|
|
31
|
+
const result = stripHtml("Plain text");
|
|
32
|
+
expect(result).toBe("Plain text");
|
|
33
|
+
});
|
|
34
|
+
it("should trim whitespace from result", () => {
|
|
35
|
+
const result = stripHtml("<p> Trimmed text </p>");
|
|
36
|
+
expect(result).toBe("Trimmed text");
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
describe("edge cases", () => {
|
|
40
|
+
it("should handle null input", () => {
|
|
41
|
+
const result = stripHtml(null);
|
|
42
|
+
expect(result).toBe("");
|
|
43
|
+
});
|
|
44
|
+
it("should handle undefined input", () => {
|
|
45
|
+
const result = stripHtml(undefined);
|
|
46
|
+
expect(result).toBe("");
|
|
47
|
+
});
|
|
48
|
+
it("should handle empty string", () => {
|
|
49
|
+
const result = stripHtml("");
|
|
50
|
+
expect(result).toBe("");
|
|
51
|
+
});
|
|
52
|
+
it("should handle non-string input", () => {
|
|
53
|
+
const result = stripHtml(123);
|
|
54
|
+
expect(result).toBe("");
|
|
55
|
+
});
|
|
56
|
+
it("should handle malformed HTML", () => {
|
|
57
|
+
const result = stripHtml("<p>Malformed <strong>content</p>");
|
|
58
|
+
expect(result).toBe("Malformed content");
|
|
59
|
+
});
|
|
60
|
+
it("should handle HTML with script tags", () => {
|
|
61
|
+
const result = stripHtml("<p>Safe content</p><script>alert('xss')</script>");
|
|
62
|
+
expect(result).toBe("Safe contentalert('xss')");
|
|
63
|
+
});
|
|
64
|
+
it("should handle self-closing tags", () => {
|
|
65
|
+
const result = stripHtml("<p>Line 1<br/>Line 2</p>");
|
|
66
|
+
expect(result).toBe("Line 1Line 2");
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
describe("error handling", () => {
|
|
70
|
+
it("should handle DOM parsing errors gracefully", () => {
|
|
71
|
+
const consoleSpy = jest.spyOn(console, "warn").mockImplementation();
|
|
72
|
+
// Create a scenario that might cause DOM parsing to fail
|
|
73
|
+
const originalCreateElement = document.createElement;
|
|
74
|
+
document.createElement = jest.fn().mockImplementation(() => {
|
|
75
|
+
throw new Error("DOM error");
|
|
76
|
+
});
|
|
77
|
+
const result = stripHtml("<p>Error content</p>");
|
|
78
|
+
expect(result).toBe("<p>Error content</p>");
|
|
79
|
+
expect(consoleSpy).toHaveBeenCalledWith("Failed to strip HTML, returning original string:", expect.any(Error));
|
|
80
|
+
document.createElement = originalCreateElement;
|
|
81
|
+
consoleSpy.mockRestore();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe("HTML entities", () => {
|
|
85
|
+
it("should handle HTML entities correctly", () => {
|
|
86
|
+
const result = stripHtml("<p>Hello & goodbye < > " '</p>");
|
|
87
|
+
expect(result).toBe("Hello & goodbye < > \" '");
|
|
88
|
+
});
|
|
89
|
+
it("should handle numeric HTML entities", () => {
|
|
90
|
+
const result = stripHtml("<p>€ © ®</p>");
|
|
91
|
+
expect(result).toBe("€ © ®");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe("whitespace handling", () => {
|
|
95
|
+
it("should preserve meaningful whitespace", () => {
|
|
96
|
+
const result = stripHtml("<p>Hello <span>beautiful</span> world</p>");
|
|
97
|
+
expect(result).toBe("Hello beautiful world");
|
|
98
|
+
});
|
|
99
|
+
it("should handle line breaks and tabs", () => {
|
|
100
|
+
const result = stripHtml("<div>Line 1\n<p>Line 2</p>\t<span>Line 3</span></div>");
|
|
101
|
+
expect(result).toBe("Line 1\nLine 2\tLine 3");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|