diff --git a/packages/dom/src/stripHTML.ts b/packages/dom/src/stripHTML.ts
index 85ae43b729fe..89d460c8fef1 100644
--- a/packages/dom/src/stripHTML.ts
+++ b/packages/dom/src/stripHTML.ts
@@ -14,10 +14,10 @@
* limitations under the License.
*/
-const buffer = document.createElement('div');
+const parser = new DOMParser();
export default function stripHTML(string: string) {
// @todo: implement a cheaper way to strip markup.
- buffer.innerHTML = string;
- return buffer.textContent || '';
+ const doc = parser.parseFromString(string, 'text/html');
+ return doc.body.textContent || '';
}
diff --git a/packages/element-library/src/text/display.tsx b/packages/element-library/src/text/display.tsx
index 7c18118853d4..b24dbb619d4d 100644
--- a/packages/element-library/src/text/display.tsx
+++ b/packages/element-library/src/text/display.tsx
@@ -22,7 +22,11 @@ import { useEffect, useRef, useMemo } from '@googleforcreators/react';
import { createSolid, type Solid } from '@googleforcreators/patterns';
import { useUnits } from '@googleforcreators/units';
import { useTransformHandler } from '@googleforcreators/transform';
-import { getHTMLFormatters, getHTMLInfo } from '@googleforcreators/rich-text';
+import {
+ getHTMLFormatters,
+ getHTMLInfo,
+ sanitizeEditorHtml,
+} from '@googleforcreators/rich-text';
import { stripHTML } from '@googleforcreators/dom';
import {
getResponsiveBorder,
@@ -314,7 +318,7 @@ function TextDisplay({
borderRadius={borderRadius}
dataToEditorY={dataToEditorY}
dangerouslySetInnerHTML={{
- __html: contentWithoutColor,
+ __html: sanitizeEditorHtml(contentWithoutColor),
}}
/>
@@ -325,7 +329,7 @@ function TextDisplay({
ref={fgRef}
{...props}
dangerouslySetInnerHTML={{
- __html: content,
+ __html: sanitizeEditorHtml(content),
}}
/>
@@ -351,7 +355,7 @@ function TextDisplay({
}
dangerouslySetInnerHTML={{
- __html: content,
+ __html: sanitizeEditorHtml(content),
}}
previewMode={previewMode}
{...props}
diff --git a/packages/element-library/src/text/frame.tsx b/packages/element-library/src/text/frame.tsx
index 7e54f483fcaa..87ebfa5f1127 100644
--- a/packages/element-library/src/text/frame.tsx
+++ b/packages/element-library/src/text/frame.tsx
@@ -26,7 +26,10 @@ import type {
TextElementFont,
FrameProps,
} from '@googleforcreators/elements';
-import { getCaretCharacterOffsetWithin } from '@googleforcreators/rich-text';
+import {
+ getCaretCharacterOffsetWithin,
+ sanitizeEditorHtml,
+} from '@googleforcreators/rich-text';
/**
* Internal dependencies
@@ -166,7 +169,7 @@ function TextFrame({
// See https://github.com/googleforcreators/web-stories-wp/issues/7745.
data-fix-caret
className="syncMargin"
- dangerouslySetInnerHTML={{ __html: content }}
+ dangerouslySetInnerHTML={{ __html: sanitizeEditorHtml(content) }}
{...props}
/>
);
diff --git a/packages/rich-text/src/formatters/color.ts b/packages/rich-text/src/formatters/color.ts
index 87470b394e2a..7c96990e34e8 100644
--- a/packages/rich-text/src/formatters/color.ts
+++ b/packages/rich-text/src/formatters/color.ts
@@ -20,8 +20,6 @@
import {
createSolid,
generatePatternStyles,
- getHexFromSolid,
- getSolidFromHex,
isPatternEqual,
createSolidFromString,
} from '@googleforcreators/patterns';
@@ -37,17 +35,7 @@ import {
togglePrefixStyle,
getPrefixStylesInSelection,
} from '../styleManipulation';
-import { isStyle, getVariable } from './util';
-
-/*
- * Color uses PREFIX-XXXXXXXX where XXXXXXXX is the 8 digit
- * hex representation of the RGBA color.
- */
-const styleToColor = (style: string): Pattern =>
- getSolidFromHex(getVariable(style, COLOR));
-
-const colorToStyle = (color: Solid): string =>
- `${COLOR}-${getHexFromSolid(color)}`;
+import { isStyle, styleToColor, colorToStyle } from './util';
function elementToStyle(element: HTMLElement): string | null {
const isSpan = element.tagName.toLowerCase() === 'span';
diff --git a/packages/rich-text/src/formatters/gradientColor.ts b/packages/rich-text/src/formatters/gradientColor.ts
index b5719257c846..de0f75510961 100644
--- a/packages/rich-text/src/formatters/gradientColor.ts
+++ b/packages/rich-text/src/formatters/gradientColor.ts
@@ -20,7 +20,6 @@
import {
createSolid,
generatePatternStyles,
- getGradientStyleFromColor,
isPatternEqual,
getColorFromGradientStyle,
type Gradient,
@@ -38,13 +37,11 @@ import {
togglePrefixStyle,
getPrefixStylesInSelection,
} from '../styleManipulation';
-import { isStyle, getVariable } from './util';
-
-const styleToColor = (style: string): Gradient =>
- getColorFromGradientStyle(getVariable(style, GRADIENT_COLOR));
-
-const colorToStyle = (color: Gradient): string =>
- `${GRADIENT_COLOR}-${getGradientStyleFromColor(color)}`;
+import {
+ isStyle,
+ styleToGradientColor as styleToColor,
+ gradientColorToStyle as colorToStyle,
+} from './util';
function elementToStyle(element: HTMLElement): string | null {
const isSpan = element.tagName.toLowerCase() === 'span';
diff --git a/packages/rich-text/src/formatters/util.ts b/packages/rich-text/src/formatters/util.ts
index b6cecb6f58bb..9aaccb06d5b6 100644
--- a/packages/rich-text/src/formatters/util.ts
+++ b/packages/rich-text/src/formatters/util.ts
@@ -18,11 +18,25 @@
* External dependencies
*/
import type { FontWeight, FontVariantStyle } from '@googleforcreators/elements';
+import {
+ getColorFromGradientStyle,
+ getGradientStyleFromColor,
+ getHexFromSolid,
+ getSolidFromHex,
+ type Gradient,
+ type Pattern,
+ type Solid,
+} from '@googleforcreators/patterns';
/**
* Internal dependencies
*/
-import { type LETTERSPACING, WEIGHT } from '../customConstants';
+import {
+ COLOR,
+ GRADIENT_COLOR,
+ type LETTERSPACING,
+ WEIGHT,
+} from '../customConstants';
export const isStyle = (style: string | undefined, prefix: string) =>
Boolean(style?.startsWith(prefix));
@@ -60,3 +74,19 @@ export function styleToNumeric(
export function weightToStyle(weight: number) {
return numericToStyle(WEIGHT, weight);
}
+
+/*
+ * Color uses PREFIX-XXXXXXXX where XXXXXXXX is the 8 digit
+ * hex representation of the RGBA color.
+ */
+export const styleToColor = (style: string): Pattern =>
+ getSolidFromHex(getVariable(style, COLOR));
+
+export const colorToStyle = (color: Solid): string =>
+ `${COLOR}-${getHexFromSolid(color)}`;
+
+export const styleToGradientColor = (style: string): Gradient =>
+ getColorFromGradientStyle(getVariable(style, GRADIENT_COLOR));
+
+export const gradientColorToStyle = (color: Gradient): string =>
+ `${GRADIENT_COLOR}-${getGradientStyleFromColor(color)}`;
diff --git a/packages/rich-text/src/getTextColors.ts b/packages/rich-text/src/getTextColors.ts
new file mode 100644
index 000000000000..763e676ec336
--- /dev/null
+++ b/packages/rich-text/src/getTextColors.ts
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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
+ *
+ * https://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.
+ */
+
+/**
+ * External dependencies
+ */
+import {
+ createSolid,
+ getHexFromSolid,
+ type Solid,
+} from '@googleforcreators/patterns';
+
+/**
+ * Internal dependencies
+ */
+import { COLOR, NONE } from './customConstants';
+import { getSelectAllStateFromHTML } from './htmlManipulation';
+import { getPrefixStylesInSelection } from './styleManipulation';
+import { styleToColor } from './formatters/util';
+
+export default function getTextColors(html: string): string[] {
+ const htmlState = getSelectAllStateFromHTML(html);
+ return getPrefixStylesInSelection(htmlState, COLOR)
+ .map((color) => {
+ if (color === NONE) {
+ return createSolid(0, 0, 0);
+ }
+
+ return styleToColor(color) as Solid;
+ })
+ .map(
+ // To remove the alpha channel.
+ (color) => '#' + getHexFromSolid(color).slice(0, 6)
+ );
+}
diff --git a/packages/rich-text/src/htmlManipulation.ts b/packages/rich-text/src/htmlManipulation.ts
index 61387cb676a3..cc3b16f1442d 100644
--- a/packages/rich-text/src/htmlManipulation.ts
+++ b/packages/rich-text/src/htmlManipulation.ts
@@ -18,6 +18,7 @@
* External dependencies
*/
import { EditorState } from 'draft-js';
+import { filterEditorState } from 'draftjs-filters';
/**
* Internal dependencies
@@ -28,6 +29,16 @@ import customImport from './customImport';
import customExport from './customExport';
import { getSelectionForAll } from './util';
import type { StyleSetter, AllowedSetterArgs } from './types';
+import {
+ ITALIC,
+ UNDERLINE,
+ WEIGHT,
+ COLOR,
+ LETTERSPACING,
+ UPPERCASE,
+ GRADIENT_COLOR,
+} from './customConstants';
+import { getPrefixStylesInSelection } from './styleManipulation';
/**
* Return an editor state object with content set to parsed HTML
@@ -60,8 +71,7 @@ function updateAndReturnHTML(
...args: [AllowedSetterArgs]
) {
const stateWithUpdate = updater(getSelectAllStateFromHTML(html), ...args);
- const renderedHTML = customExport(stateWithUpdate);
- return renderedHTML;
+ return customExport(stateWithUpdate);
}
const getHTMLFormatter =
@@ -90,3 +100,32 @@ export function getHTMLInfo(html: string) {
const htmlStateInfo = getStateInfo(getSelectAllStateFromHTML(html));
return htmlStateInfo;
}
+
+export function sanitizeEditorHtml(html: string) {
+ const editorState = getSelectAllStateFromHTML(html);
+
+ const styles: string[] = [
+ ...getPrefixStylesInSelection(editorState, ITALIC),
+ ...getPrefixStylesInSelection(editorState, UNDERLINE),
+ ...getPrefixStylesInSelection(editorState, WEIGHT),
+ ...getPrefixStylesInSelection(editorState, COLOR),
+ ...getPrefixStylesInSelection(editorState, LETTERSPACING),
+ ...getPrefixStylesInSelection(editorState, UPPERCASE),
+ ...getPrefixStylesInSelection(editorState, GRADIENT_COLOR),
+ ];
+
+ return (
+ customExport(
+ filterEditorState(
+ {
+ blocks: [],
+ styles,
+ entities: [],
+ maxNesting: 1,
+ whitespacedCharacters: [],
+ },
+ editorState
+ )
+ ) || ''
+ );
+}
diff --git a/packages/rich-text/src/index.ts b/packages/rich-text/src/index.ts
index 1ad875eee0c0..875e4ae4e835 100644
--- a/packages/rich-text/src/index.ts
+++ b/packages/rich-text/src/index.ts
@@ -23,6 +23,7 @@ export { default as RichTextContext } from './context';
export { default as useRichText } from './useRichText';
export { default as usePasteTextContent } from './usePasteTextContent';
export { default as getFontVariants } from './getFontVariants';
+export { default as getTextColors } from './getTextColors';
export { default as getCaretCharacterOffsetWithin } from './utils/getCaretCharacterOffsetWithin';
export * from './htmlManipulation';
diff --git a/packages/story-editor/src/components/checklist/utils/getSpansFromContent.js b/packages/rich-text/src/test/getTextColors.ts
similarity index 51%
rename from packages/story-editor/src/components/checklist/utils/getSpansFromContent.js
rename to packages/rich-text/src/test/getTextColors.ts
index 9ec25e0950ce..df79b7a69e8f 100644
--- a/packages/story-editor/src/components/checklist/utils/getSpansFromContent.js
+++ b/packages/rich-text/src/test/getTextColors.ts
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 Google LLC
+ * Copyright 2020 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,22 +14,19 @@
* limitations under the License.
*/
-let spansFromContentBuffer;
/**
- *
- * @param {string} content the buffer containing text element content
- * @return {Array} list of individual span elements from the content
+ * Internal dependencies
*/
-export function getSpansFromContent(content) {
- // memoize buffer
- if (!spansFromContentBuffer) {
- spansFromContentBuffer = document.createElement('div');
- }
+import getTextColors from '../getTextColors';
+
+describe('getTextColors', () => {
+ it('should return a list of text colors', () => {
+ const htmlContent =
+ 'Fill in some text';
+ const expected = ['#000000', '#eb0404', '#026111'];
- spansFromContentBuffer.innerHTML = content;
+ const actual = getTextColors(htmlContent);
- // return Array instead of HtmlCollection
- return Array.prototype.slice.call(
- spansFromContentBuffer.getElementsByTagName('span')
- );
-}
+ expect(actual).toStrictEqual(expected);
+ });
+});
diff --git a/packages/story-editor/src/components/checklist/checks/pageBackgroundLowTextContrast/check.js b/packages/story-editor/src/components/checklist/checks/pageBackgroundLowTextContrast/check.js
index bf11ed88e270..8eab4ce1708a 100644
--- a/packages/story-editor/src/components/checklist/checks/pageBackgroundLowTextContrast/check.js
+++ b/packages/story-editor/src/components/checklist/checks/pageBackgroundLowTextContrast/check.js
@@ -27,6 +27,7 @@ import {
preloadImage,
} from '@googleforcreators/media';
import { createSolidFromString } from '@googleforcreators/patterns';
+import { getTextColors } from '@googleforcreators/rich-text';
/**
* Internal dependencies
@@ -36,7 +37,6 @@ import {
calculateLuminanceFromStyleColor,
checkContrastFromLuminances,
} from '../../../../utils/contrastUtils';
-import { getSpansFromContent } from '../../utils';
import getMediaBaseColor from '../../../../utils/getMediaBaseColor';
import { noop } from '../../../../utils/noop';
@@ -276,19 +276,7 @@ async function getOverlapBgColor({ bgImage, bgBox, overlapBox }) {
* @return {Array} the style colors from the span tags in text element content
*/
function getTextStyleColors(element) {
- const spans = getSpansFromContent(element.content);
- const textStyleColors = spans
- .map((span) => span.style?.color)
- .filter(Boolean);
- // if no colors were retrieved but there are spans, there is a black default color
- const noColorStyleOnSpans =
- textStyleColors.length === 0 && spans.length !== 0;
- // if no spans were retrieved but there is content, there is a black default color
- const noSpans = element.content.length !== 0 && spans.length === 0;
- if (noColorStyleOnSpans || noSpans) {
- textStyleColors.push('rgb(0, 0, 0)');
- }
- return textStyleColors;
+ return getTextColors(element.content);
}
function getTextShapeBackgroundColor({ background }) {
diff --git a/packages/story-editor/src/components/checklist/utils/index.js b/packages/story-editor/src/components/checklist/utils/index.js
index 1463997c0b9d..addb438fac0a 100644
--- a/packages/story-editor/src/components/checklist/utils/index.js
+++ b/packages/story-editor/src/components/checklist/utils/index.js
@@ -18,5 +18,4 @@ export { characterCountForPage } from './characterCountForPage';
export { filterStoryPages } from './filterStoryPages';
export { filterStoryElements } from './filterStoryElements';
export { getVisibleThumbnails } from './getVisibleThumbnails';
-export { getSpansFromContent } from './getSpansFromContent';
export { ThumbnailPagePreview } from './thumbnailPagePreview';