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';