Skip to content

Commit e88f310

Browse files
committed
feat: add stripStyles option to remove inline styles from HTML elements
1 parent 3d8d2a4 commit e88f310

File tree

3 files changed

+202
-3
lines changed

3 files changed

+202
-3
lines changed

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ it with React.
3232
These will be used, unless the element already has the attribute set. The only
3333
exception is the `className` attribute, which will be merged with the default
3434
value.
35+
- `stripStyles`: Remove inline style attributes from HTML elements. Accepts:
36+
- `true`: Removes all inline styles from all HTML elements
37+
- An object with configuration options:
38+
- `tags`: Array of HTML tags from which to strip styles. If not provided,
39+
styles are stripped from all tags.
40+
- `except`: Array of HTML tags that should keep their styles, even if they
41+
are in the `tags` array.
42+
- Default is `false` (all inline styles are preserved).
3543

3644
When passing the `renderBlock` and `renderNode` props, consider making them
3745
static functions (move them outside the consuming component) to avoid
@@ -78,7 +86,11 @@ function RichText({ data }) {
7886
data={data.richText}
7987
renderNode={renderNode}
8088
renderBlock={renderBlock}
81-
htmlAttributes={{ p: { className: "mb-4" }}
89+
htmlAttributes={{ p: { className: "mb-4" } }}
90+
stripStyles={{
91+
// Strip styles from all tags except the following:
92+
except: ["img"], // Keep styles on `img` tags
93+
}}
8294
/>
8395
);
8496
}

src/rich-text/UmbracoRichText.tsx

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,20 @@ interface RichTextProps {
6969
htmlAttributes?: Partial<{
7070
[Tag in keyof React.JSX.IntrinsicElements]: React.JSX.IntrinsicElements[Tag];
7171
}>;
72+
/**
73+
* Strip the inline style attributes from the HTML elements
74+
* This can be a boolean to strip all styles, or an object to specify which tags to strip styles from.
75+
* If an object is provided, the `tags` property lists tags to strip styles from. If not set, all tags will have their styles stripped.
76+
* The `except` property can be used to specify tags that should not have their styles stripped, even if they are in the `tags` array.
77+
*
78+
* @default false
79+
*/
80+
stripStyles?:
81+
| boolean
82+
| {
83+
tags?: Array<keyof React.JSX.IntrinsicElements>;
84+
except?: Array<keyof React.JSX.IntrinsicElements>;
85+
};
7286
}
7387

7488
function parseUrl(href: string) {
@@ -94,12 +108,16 @@ function RichTextElement({
94108
renderBlock,
95109
renderNode,
96110
htmlAttributes = {},
111+
stripStyles = false,
97112
meta,
98113
}: {
99114
element: RichTextElementModel;
100115
blocks: Array<RenderBlockContext> | undefined;
101116
meta: NodeMeta | undefined;
102-
} & Pick<RichTextProps, "renderBlock" | "renderNode" | "htmlAttributes">) {
117+
} & Pick<
118+
RichTextProps,
119+
"renderBlock" | "renderNode" | "htmlAttributes" | "stripStyles"
120+
>) {
103121
if (!element || element.tag === "#comment" || element.tag === "#root")
104122
return null;
105123

@@ -136,6 +154,7 @@ function RichTextElement({
136154
blocks={blocks}
137155
renderBlock={renderBlock}
138156
renderNode={renderNode}
157+
stripStyles={stripStyles}
139158
meta={{
140159
ancestor: element,
141160
children: hasElements(node) ? node.elements : undefined,
@@ -189,8 +208,32 @@ function RichTextElement({
189208
}
190209
}
191210

211+
// Handle style attributes
192212
if (typeof style === "string") {
193-
attributes.style = parseStyle(style);
213+
// Determine if we should strip styles for this element
214+
let shouldStripStyle = stripStyles === true;
215+
216+
if (typeof stripStyles === "object") {
217+
// If tags array is provided, only strip styles from those tags
218+
// If tags is not provided, strip from all tags
219+
const shouldStrip =
220+
stripStyles.tags?.includes(
221+
element.tag as keyof React.JSX.IntrinsicElements,
222+
) ?? true;
223+
224+
// Check if this tag is in the except list
225+
const isExcepted =
226+
stripStyles.except?.includes(
227+
element.tag as keyof React.JSX.IntrinsicElements,
228+
) || false;
229+
230+
shouldStripStyle = shouldStrip && !isExcepted;
231+
}
232+
233+
// Only parse and add style if we're not stripping it
234+
if (!shouldStripStyle) {
235+
attributes.style = parseStyle(style);
236+
}
194237
}
195238

196239
if (renderNode) {
@@ -250,6 +293,7 @@ export function UmbracoRichText(props: RichTextProps) {
250293
renderBlock={props.renderBlock}
251294
renderNode={props.renderNode}
252295
htmlAttributes={props.htmlAttributes}
296+
stripStyles={props.stripStyles}
253297
meta={{
254298
ancestor: rootElement,
255299
children: hasElements(element) ? element.elements : undefined,

src/rich-text/__tests__/UmbracoRichText.browser.test.tsx

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,3 +700,146 @@ it("should convert colspan and rowspan to colSpan and rowSpan in table rendering
700700
expect(screen.getByTestId("cell1").element()).toHaveAttribute("colSpan", "2");
701701
expect(screen.getByTestId("cell2").element()).toHaveAttribute("rowSpan", "2");
702702
});
703+
704+
it("should strip all inline styles when stripStyles is true", () => {
705+
const screen = render(
706+
<UmbracoRichText
707+
data={{
708+
tag: "#root",
709+
elements: [
710+
{
711+
tag: "div",
712+
attributes: { style: "color: red; font-size: 16px;" },
713+
elements: [
714+
{
715+
tag: "p",
716+
attributes: { style: "margin: 10px; padding: 5px;" },
717+
elements: [{ tag: "#text", text: "Text with stripped styles" }],
718+
},
719+
],
720+
},
721+
],
722+
}}
723+
stripStyles={true}
724+
/>,
725+
);
726+
727+
const div = screen.container.querySelector("div");
728+
const p = screen.container.querySelector("p");
729+
730+
expect(div).not.toHaveAttribute("style");
731+
expect(p).not.toHaveAttribute("style");
732+
});
733+
734+
it("should strip styles only from specified tags", () => {
735+
const screen = render(
736+
<UmbracoRichText
737+
data={{
738+
tag: "#root",
739+
elements: [
740+
{
741+
tag: "div",
742+
attributes: { style: "color: red; font-size: 16px;" },
743+
elements: [
744+
{
745+
tag: "p",
746+
attributes: { style: "margin: 10px; padding: 5px;" },
747+
elements: [
748+
{ tag: "#text", text: "Text with partially stripped styles" },
749+
],
750+
},
751+
],
752+
},
753+
],
754+
}}
755+
stripStyles={{ tags: ["p"] }}
756+
/>,
757+
);
758+
759+
const div = screen.container.querySelector("div");
760+
const p = screen.container.querySelector("p");
761+
762+
expect(div).toHaveAttribute("style", "color: red; font-size: 16px;");
763+
expect(p).not.toHaveAttribute("style");
764+
});
765+
766+
it("should preserve styles for excepted tags", () => {
767+
const screen = render(
768+
<UmbracoRichText
769+
data={{
770+
tag: "#root",
771+
elements: [
772+
{
773+
tag: "div",
774+
attributes: { style: "color: red; font-size: 16px;" },
775+
elements: [
776+
{
777+
tag: "p",
778+
attributes: { style: "margin: 10px; padding: 5px;" },
779+
elements: [{ tag: "#text", text: "Text with excepted styles" }],
780+
},
781+
{
782+
tag: "span",
783+
attributes: { style: "font-weight: bold;" },
784+
elements: [{ tag: "#text", text: "Bold text" }],
785+
},
786+
],
787+
},
788+
],
789+
}}
790+
stripStyles={{ except: ["p"] }}
791+
/>,
792+
);
793+
794+
const div = screen.container.querySelector("div");
795+
const p = screen.container.querySelector("p");
796+
const span = screen.container.querySelector("span");
797+
798+
expect(div).not.toHaveAttribute("style");
799+
expect(p).toHaveAttribute("style", "margin: 10px; padding: 5px;");
800+
expect(span).not.toHaveAttribute("style");
801+
});
802+
803+
it("should combine tags and except properties correctly", () => {
804+
const screen = render(
805+
<UmbracoRichText
806+
data={{
807+
tag: "#root",
808+
elements: [
809+
{
810+
tag: "div",
811+
attributes: { style: "color: red;" },
812+
elements: [
813+
{
814+
tag: "p",
815+
attributes: { style: "margin: 10px;" },
816+
elements: [{ tag: "#text", text: "Paragraph" }],
817+
},
818+
{
819+
tag: "span",
820+
attributes: { style: "font-weight: bold;" },
821+
elements: [{ tag: "#text", text: "Span" }],
822+
},
823+
{
824+
tag: "h1",
825+
attributes: { style: "font-size: 24px;" },
826+
elements: [{ tag: "#text", text: "Heading" }],
827+
},
828+
],
829+
},
830+
],
831+
}}
832+
stripStyles={{ tags: ["div", "p", "span"], except: ["span"] }}
833+
/>,
834+
);
835+
836+
const div = screen.container.querySelector("div");
837+
const p = screen.container.querySelector("p");
838+
const span = screen.container.querySelector("span");
839+
const h1 = screen.container.querySelector("h1");
840+
841+
expect(div).not.toHaveAttribute("style");
842+
expect(p).not.toHaveAttribute("style");
843+
expect(span).toHaveAttribute("style", "font-weight: bold;");
844+
expect(h1).toHaveAttribute("style", "font-size: 24px;");
845+
});

0 commit comments

Comments
 (0)