From 719a86e5e1e3fdef58af41eb160ab43ba49d19c9 Mon Sep 17 00:00:00 2001 From: Yuzhong Zhang Date: Tue, 25 Aug 2026 14:35:55 +0000 Subject: [PATCH] fix: fall back to default markdownStyle colors when processColor fails Invalid names such as tranparent become undefined and crash iOS when building NSAttributedString. Keep valid colors unchanged. Fixes #702 --- src/MarkdownTextInput.tsx | 26 +----------- src/__tests__/processMarkdownStyle.test.ts | 46 ++++++++++++++++++++++ src/styleUtils.ts | 29 +++++++++++++- 3 files changed, 75 insertions(+), 26 deletions(-) create mode 100644 src/__tests__/processMarkdownStyle.test.ts diff --git a/src/MarkdownTextInput.tsx b/src/MarkdownTextInput.tsx index 935b0554..9f1fc996 100644 --- a/src/MarkdownTextInput.tsx +++ b/src/MarkdownTextInput.tsx @@ -1,12 +1,11 @@ -import {StyleSheet, TextInput, processColor} from 'react-native'; +import {StyleSheet, TextInput} from 'react-native'; import React from 'react'; import type {TextInputProps} from 'react-native'; import {createSerializable, createWorkletRuntime} from 'react-native-worklets'; import type {SerializableRef, WorkletFunction, WorkletRuntime} from 'react-native-worklets'; import MarkdownTextInputDecoratorViewNativeComponent from './MarkdownTextInputDecoratorViewNativeComponent'; -import type {MarkdownStyle} from './MarkdownTextInputDecoratorViewNativeComponent'; import NativeLiveMarkdownModule from './NativeLiveMarkdownModule'; -import {mergeMarkdownStyleWithDefault} from './styleUtils'; +import {processMarkdownStyle} from './styleUtils'; import type {PartialMarkdownStyle} from './styleUtils'; import type {InlineImagesInputProps, MarkdownRange} from './commonTypes'; @@ -70,27 +69,6 @@ type FormatSelectionResult = { type MarkdownTextInput = TextInput & React.Component; -function processColorsInMarkdownStyle(input: MarkdownStyle): MarkdownStyle { - const output = JSON.parse(JSON.stringify(input)); - - Object.keys(output).forEach((key) => { - const obj = output[key]; - Object.keys(obj).forEach((prop) => { - // TODO: use ReactNativeStyleAttributes from 'react-native/Libraries/Components/View/ReactNativeStyleAttributes' - if (!(prop === 'color' || prop.endsWith('Color'))) { - return; - } - obj[prop] = processColor(obj[prop]); - }); - }); - - return output as MarkdownStyle; -} - -function processMarkdownStyle(input: PartialMarkdownStyle | undefined): MarkdownStyle { - return processColorsInMarkdownStyle(mergeMarkdownStyleWithDefault(input)); -} - const MarkdownTextInput = React.forwardRef((props, ref) => { const markdownStyle = React.useMemo(() => processMarkdownStyle(props.markdownStyle), [props.markdownStyle]); diff --git a/src/__tests__/processMarkdownStyle.test.ts b/src/__tests__/processMarkdownStyle.test.ts new file mode 100644 index 00000000..a20768a6 --- /dev/null +++ b/src/__tests__/processMarkdownStyle.test.ts @@ -0,0 +1,46 @@ +import {processColor} from 'react-native'; +import {processMarkdownStyle} from '../styleUtils'; + +describe('processMarkdownStyle', () => { + it('falls back to the default color when markdownStyle has an invalid color name', () => { + const style = processMarkdownStyle({ + pre: { + backgroundColor: 'tranparent', + }, + }); + + expect(style.pre.backgroundColor).toBe(processColor('lightgray')); + }); + + it('falls back to the default color when markdownStyle uses CSS none', () => { + const style = processMarkdownStyle({ + pre: { + backgroundColor: 'none', + }, + }); + + expect(style.pre.backgroundColor).toBe(processColor('lightgray')); + }); + + it('still processes valid colors', () => { + const style = processMarkdownStyle({ + pre: { + backgroundColor: 'red', + }, + }); + + expect(style.pre.backgroundColor).toBe(processColor('red')); + }); + + it('does not let one invalid color wipe other processed colors', () => { + const style = processMarkdownStyle({ + pre: { + color: 'blue', + backgroundColor: 'tranparent', + }, + }); + + expect(style.pre.color).toBe(processColor('blue')); + expect(style.pre.backgroundColor).toBe(processColor('lightgray')); + }); +}); diff --git a/src/styleUtils.ts b/src/styleUtils.ts index c97fd1e4..0ce55e40 100644 --- a/src/styleUtils.ts +++ b/src/styleUtils.ts @@ -1,4 +1,4 @@ -import {Platform} from 'react-native'; +import {Platform, processColor} from 'react-native'; import type {MarkdownStyle} from './MarkdownTextInputDecoratorViewNativeComponent'; type PartialMarkdownStyle = Partial<{ @@ -116,6 +116,31 @@ function parseStringWithUnitToNumber(value: string | number | null): number { return value ? parseInt(value.replace('px', ''), 10) : 0; } +function processColorsInMarkdownStyle(input: MarkdownStyle): MarkdownStyle { + const output = JSON.parse(JSON.stringify(input)); + const defaults = JSON.parse(JSON.stringify(makeDefaultMarkdownStyle())); + + Object.keys(output).forEach((key) => { + const obj = output[key]; + const defaultObj = defaults[key]; + Object.keys(obj).forEach((prop) => { + // TODO: use ReactNativeStyleAttributes from 'react-native/Libraries/Components/View/ReactNativeStyleAttributes' + if (!(prop === 'color' || prop.endsWith('Color'))) { + return; + } + const processed = processColor(obj[prop]); + // processColor returns null/undefined for invalid names; nil colors crash iOS. + obj[prop] = processed ?? processColor(defaultObj?.[prop]); + }); + }); + + return output as MarkdownStyle; +} + +function processMarkdownStyle(input: PartialMarkdownStyle | undefined): MarkdownStyle { + return processColorsInMarkdownStyle(mergeMarkdownStyleWithDefault(input)); +} + export type {PartialMarkdownStyle}; -export {mergeMarkdownStyleWithDefault, parseStringWithUnitToNumber}; +export {mergeMarkdownStyleWithDefault, parseStringWithUnitToNumber, processMarkdownStyle};