diff --git a/android/src/main/java/com/swmansion/enriched/markdown/spoiler/SpoilerParticleDrawable.kt b/android/src/main/java/com/swmansion/enriched/markdown/spoiler/SpoilerParticleDrawable.kt index 807fde5e..f6e33926 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/spoiler/SpoilerParticleDrawable.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/spoiler/SpoilerParticleDrawable.kt @@ -3,7 +3,6 @@ package com.swmansion.enriched.markdown.spoiler import android.graphics.Canvas import android.graphics.Color import android.graphics.Paint -import com.swmansion.enriched.markdown.styles.SpoilerStyle import kotlin.math.cos import kotlin.math.max import kotlin.math.sin @@ -30,8 +29,8 @@ class SpoilerParticleDrawable( private var accumulatedPrimaryBirths = 0f private var accumulatedSecondaryBirths = 0f - private val densityFactor = particleDensity / SpoilerStyle.DEFAULT_PARTICLE_DENSITY - private val speedFactor = particleSpeed / SpoilerStyle.DEFAULT_PARTICLE_SPEED + private val densityFactor = particleDensity / BASE_PARTICLE_DENSITY + private val speedFactor = particleSpeed / BASE_PARTICLE_SPEED private var isRevealing = false private var revealStartTime = -1L @@ -231,5 +230,8 @@ class SpoilerParticleDrawable( private const val PARTICLE_LIFETIME = 7 private const val PARTICLE_AGE = 8 private const val STRIDE = 9 + + private const val BASE_PARTICLE_DENSITY = 8f + private const val BASE_PARTICLE_SPEED = 20f } } diff --git a/android/src/main/java/com/swmansion/enriched/markdown/styles/SpoilerStyle.kt b/android/src/main/java/com/swmansion/enriched/markdown/styles/SpoilerStyle.kt index 645d0452..57a3608b 100644 --- a/android/src/main/java/com/swmansion/enriched/markdown/styles/SpoilerStyle.kt +++ b/android/src/main/java/com/swmansion/enriched/markdown/styles/SpoilerStyle.kt @@ -9,19 +9,19 @@ data class SpoilerStyle( val solidBorderRadius: Float, ) { companion object { - const val DEFAULT_PARTICLE_DENSITY = 8.0f - const val DEFAULT_PARTICLE_SPEED = 20.0f - const val DEFAULT_SOLID_BORDER_RADIUS = 4.0f - fun fromReadableMap( map: ReadableMap, parser: StyleParser, ): SpoilerStyle { val color = parser.parseColor(map, "color") - val particleDensity = parser.parseOptionalDouble(map, "particleDensity", DEFAULT_PARTICLE_DENSITY.toDouble()).toFloat() - val particleSpeed = parser.parseOptionalDouble(map, "particleSpeed", DEFAULT_PARTICLE_SPEED.toDouble()).toFloat() - val solidBorderRadius = parser.parseOptionalDouble(map, "solidBorderRadius", DEFAULT_SOLID_BORDER_RADIUS.toDouble()).toFloat() - return SpoilerStyle(color, particleDensity, particleSpeed, solidBorderRadius) + val particlesMap = map.getMap("particles")!! + val solidMap = map.getMap("solid")!! + return SpoilerStyle( + color = color, + particleDensity = particlesMap.getDouble("density").toFloat(), + particleSpeed = particlesMap.getDouble("speed").toFloat(), + solidBorderRadius = solidMap.getDouble("borderRadius").toFloat(), + ) } } } diff --git a/docs/STYLES.md b/docs/STYLES.md index dde714c3..fdc0009a 100644 --- a/docs/STYLES.md +++ b/docs/STYLES.md @@ -376,7 +376,7 @@ Styles for inline LaTeX math (`$...$`). Inline math is rendered within the surro ### Spoiler-specific -Styles for spoiler text (`||hidden text||`). Spoiler text is concealed behind an animated particle overlay until the user taps to reveal it. +Styles for spoiler text (`||hidden text||`). Spoiler text is concealed behind an overlay (controlled by the `spoilerOverlay` prop) until the user taps to reveal it. | Property | Type | Description | |----------|------|-------------| diff --git a/ios/styles/StyleConfig.mm b/ios/styles/StyleConfig.mm index 4762cd47..3b4e668e 100644 --- a/ios/styles/StyleConfig.mm +++ b/ios/styles/StyleConfig.mm @@ -255,9 +255,6 @@ - (instancetype)init _tableFontNeedsRecreation = YES; _tableHeaderFontNeedsRecreation = YES; _linkUnderline = YES; - _spoilerParticleDensity = 8.0; - _spoilerParticleSpeed = 20.0; - _spoilerSolidBorderRadius = 4.0; return self; } diff --git a/ios/utils/StylePropsUtils.h b/ios/utils/StylePropsUtils.h index bc16781e..f74311aa 100644 --- a/ios/utils/StylePropsUtils.h +++ b/ios/utils/StylePropsUtils.h @@ -1049,18 +1049,18 @@ BOOL applyMarkdownStyleToConfig(StyleConfig *config, const MarkdownStyle &newSty changed = YES; } - if (newStyle.spoiler.particleDensity != oldStyle.spoiler.particleDensity) { - [config setSpoilerParticleDensity:newStyle.spoiler.particleDensity]; + if (newStyle.spoiler.particles.density != oldStyle.spoiler.particles.density) { + [config setSpoilerParticleDensity:newStyle.spoiler.particles.density]; changed = YES; } - if (newStyle.spoiler.particleSpeed != oldStyle.spoiler.particleSpeed) { - [config setSpoilerParticleSpeed:newStyle.spoiler.particleSpeed]; + if (newStyle.spoiler.particles.speed != oldStyle.spoiler.particles.speed) { + [config setSpoilerParticleSpeed:newStyle.spoiler.particles.speed]; changed = YES; } - if (newStyle.spoiler.solidBorderRadius != oldStyle.spoiler.solidBorderRadius) { - [config setSpoilerSolidBorderRadius:newStyle.spoiler.solidBorderRadius]; + if (newStyle.spoiler.solid.borderRadius != oldStyle.spoiler.solid.borderRadius) { + [config setSpoilerSolidBorderRadius:newStyle.spoiler.solid.borderRadius]; changed = YES; } diff --git a/src/EnrichedMarkdownNativeComponent.ts b/src/EnrichedMarkdownNativeComponent.ts index 1c394c3e..e81e2d96 100644 --- a/src/EnrichedMarkdownNativeComponent.ts +++ b/src/EnrichedMarkdownNativeComponent.ts @@ -137,11 +137,19 @@ interface InlineMathStyleInternal { color: ColorValue; } +interface SpoilerParticlesStyleInternal { + density: CodegenTypes.Float; + speed: CodegenTypes.Float; +} + +interface SpoilerSolidStyleInternal { + borderRadius: CodegenTypes.Float; +} + interface SpoilerStyleInternal { color: ColorValue; - particleDensity: CodegenTypes.Float; - particleSpeed: CodegenTypes.Float; - solidBorderRadius: CodegenTypes.Float; + particles: SpoilerParticlesStyleInternal; + solid: SpoilerSolidStyleInternal; } export interface MarkdownStyleInternal { diff --git a/src/EnrichedMarkdownTextNativeComponent.ts b/src/EnrichedMarkdownTextNativeComponent.ts index fcbe4f27..a76a48d6 100644 --- a/src/EnrichedMarkdownTextNativeComponent.ts +++ b/src/EnrichedMarkdownTextNativeComponent.ts @@ -137,11 +137,19 @@ interface InlineMathStyleInternal { color: ColorValue; } +interface SpoilerParticlesStyleInternal { + density: CodegenTypes.Float; + speed: CodegenTypes.Float; +} + +interface SpoilerSolidStyleInternal { + borderRadius: CodegenTypes.Float; +} + interface SpoilerStyleInternal { color: ColorValue; - particleDensity: CodegenTypes.Float; - particleSpeed: CodegenTypes.Float; - solidBorderRadius: CodegenTypes.Float; + particles: SpoilerParticlesStyleInternal; + solid: SpoilerSolidStyleInternal; } export interface MarkdownStyleInternal { diff --git a/src/normalizeMarkdownInputStyle.ts b/src/normalizeMarkdownInputStyle.ts index 2cefd0c9..9cea7729 100644 --- a/src/normalizeMarkdownInputStyle.ts +++ b/src/normalizeMarkdownInputStyle.ts @@ -1,5 +1,6 @@ import { processColor, type ColorValue } from 'react-native'; import type { MarkdownInputStyle } from './EnrichedMarkdownInput'; +import { normalizeColor } from './styleUtils'; interface MarkdownInputStyleInternal { strong: { @@ -18,9 +19,6 @@ interface MarkdownInputStyleInternal { }; } -const normalizeColor = (color: string | undefined): ColorValue | undefined => - color ? processColor(color) : undefined; - const DEFAULT_LINK_COLOR = '#2563EB'; const DEFAULT_SPOILER_COLOR = '#374151'; const DEFAULT_SPOILER_BG_COLOR = '#E5E7EB'; diff --git a/src/normalizeMarkdownStyle.ts b/src/normalizeMarkdownStyle.ts index 5d248b51..b9b1c2fa 100644 --- a/src/normalizeMarkdownStyle.ts +++ b/src/normalizeMarkdownStyle.ts @@ -1,25 +1,11 @@ -import { Platform, processColor } from 'react-native'; +import { Platform } from 'react-native'; import type { MarkdownStyle } from './types/MarkdownStyle'; import type { BlockTextAlign, EmphasisFontStyle, MarkdownStyleInternal, } from './types/MarkdownStyleInternal'; -import { flattenSpoilerStyle, isStyleEqual } from './styleUtils'; - -// On native, processColor converts hex strings to ARGB integers the renderer -// expects. On web, CSS accepts hex strings natively — no conversion needed. -// MarkdownStyleInternal types colors as `string`; native consumers -// (EnrichedMarkdownTextNativeComponent) accept `ColorValue` (string | number) -// at runtime, so the ARGB integers processColor produces are handled correctly. -export const normalizeColor = ( - color: string | undefined -): string | undefined => - color - ? Platform.OS === 'web' - ? color - : ((processColor(color) ?? undefined) as string | undefined) - : undefined; +import { isStyleEqual, normalizeColor, mergeSubStyle } from './styleUtils'; const getSystemFont = (): string => Platform.select({ @@ -37,25 +23,6 @@ const getMonospaceFont = (): string => default: 'monospace', })!; -function mergeSubStyle>( - defaultStyle: T, - userStyle?: Partial -): T { - if (!userStyle) return defaultStyle; - const result: Record = { ...defaultStyle, ...userStyle }; - // Normalize any user-supplied color strings. On web this is a no-op (CSS - // accepts hex strings); on native it converts them to ARGB integers. - for (const key in result) { - if ( - key.toLowerCase().includes('color') && - typeof result[key] === 'string' - ) { - result[key] = normalizeColor(result[key] as string); - } - } - return result as T; -} - const defaultTextColor = normalizeColor('#1F2937')!; const defaultHeadingColor = normalizeColor('#111827')!; @@ -75,7 +42,7 @@ const baseHeader: { textAlign: 'auto', }; -const DEFAULT_NORMALIZED_STYLE: MarkdownStyleInternal = Object.freeze({ +const DEFAULT_NORMALIZED_STYLE = Object.freeze({ paragraph: { fontSize: 16, fontFamily: getSystemFont(), @@ -234,11 +201,10 @@ const DEFAULT_NORMALIZED_STYLE: MarkdownStyleInternal = Object.freeze({ }, spoiler: { color: normalizeColor('#374151')!, - particleDensity: 8, - particleSpeed: 20, - solidBorderRadius: 4, + particles: { density: 8, speed: 20 }, + solid: { borderRadius: 4 }, }, -}); +}) as MarkdownStyleInternal; const refCache = new WeakMap(); const structuralCache: { @@ -272,10 +238,9 @@ export const normalizeMarkdownStyle = ( ( Object.keys(DEFAULT_NORMALIZED_STYLE) as (keyof MarkdownStyleInternal)[] ).forEach((key) => { - const userValue = - key === 'spoiler' - ? flattenSpoilerStyle(style.spoiler) - : (style[key] as unknown as Record | undefined); + const userValue = style[key] as unknown as + | Record + | undefined; result[key] = mergeSubStyle( DEFAULT_NORMALIZED_STYLE[key] as unknown as Record, userValue as Record | undefined diff --git a/src/normalizeMarkdownStyle.web.ts b/src/normalizeMarkdownStyle.web.ts index 9a1b5311..5167d3f4 100644 --- a/src/normalizeMarkdownStyle.web.ts +++ b/src/normalizeMarkdownStyle.web.ts @@ -4,21 +4,13 @@ import type { EmphasisFontStyle, MarkdownStyleInternal, } from './types/MarkdownStyleInternal'; -import { flattenSpoilerStyle, isStyleEqual } from './styleUtils'; +import { isStyleEqual, mergeSubStyle } from './styleUtils'; const SYSTEM_FONT = 'system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif'; const MONOSPACE_FONT = 'ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace'; -function mergeSubStyle>( - defaultStyle: T, - userStyle?: Partial -): T { - if (!userStyle) return defaultStyle; - return { ...defaultStyle, ...userStyle }; -} - const defaultTextColor = '#1F2937'; const defaultHeadingColor = '#111827'; @@ -190,9 +182,8 @@ const DEFAULT_NORMALIZED_STYLE: MarkdownStyleInternal = Object.freeze({ // Spoiler rendering is not supported on web yet — defaults kept for type compatibility. spoiler: { color: '#374151', - particleDensity: 8, - particleSpeed: 20, - solidBorderRadius: 4, + particles: { density: 8, speed: 20 }, + solid: { borderRadius: 4 }, }, }); @@ -228,10 +219,9 @@ export const normalizeMarkdownStyle = ( ( Object.keys(DEFAULT_NORMALIZED_STYLE) as (keyof MarkdownStyleInternal)[] ).forEach((key) => { - const userValue = - key === 'spoiler' - ? flattenSpoilerStyle(style.spoiler) - : (style[key] as unknown as Record | undefined); + const userValue = style[key] as unknown as + | Record + | undefined; result[key] = mergeSubStyle( DEFAULT_NORMALIZED_STYLE[key] as unknown as Record, userValue as Record | undefined diff --git a/src/styleUtils.ts b/src/styleUtils.ts index 24afa112..ad339bad 100644 --- a/src/styleUtils.ts +++ b/src/styleUtils.ts @@ -1,21 +1,44 @@ +import { Platform, processColor, type ColorValue } from 'react-native'; import type { MarkdownStyle } from './types/MarkdownStyle'; -import type { MarkdownStyleInternal } from './types/MarkdownStyleInternal'; -export function flattenSpoilerStyle( - userSpoiler: MarkdownStyle['spoiler'] -): Partial | undefined { - if (!userSpoiler) return undefined; - const flat: Record = {}; - if (userSpoiler.color !== undefined) flat.color = userSpoiler.color; - if (userSpoiler.particles?.density !== undefined) - flat.particleDensity = userSpoiler.particles.density; - if (userSpoiler.particles?.speed !== undefined) - flat.particleSpeed = userSpoiler.particles.speed; - if (userSpoiler.solid?.borderRadius !== undefined) - flat.solidBorderRadius = userSpoiler.solid.borderRadius; - return Object.keys(flat).length > 0 - ? (flat as Partial) - : undefined; +export const normalizeColor = ( + color: string | undefined +): ColorValue | undefined => { + if (!color) return undefined; + if (Platform.OS === 'web') return color; + return processColor(color) ?? undefined; +}; + +export function mergeSubStyle>( + defaultStyle: T, + userStyle?: Partial +): T { + if (!userStyle) return defaultStyle; + const result: Record = { ...defaultStyle, ...userStyle }; + for (const key in result) { + const defaultValue = defaultStyle[key]; + const userValue = userStyle[key]; + if ( + typeof defaultValue === 'object' && + defaultValue !== null && + !Array.isArray(defaultValue) && + typeof userValue === 'object' && + userValue !== null && + !Array.isArray(userValue) + ) { + result[key] = { + ...(defaultValue as Record), + ...(userValue as Record), + }; + } + if ( + key.toLowerCase().includes('color') && + typeof result[key] === 'string' + ) { + result[key] = normalizeColor(result[key] as string); + } + } + return result as T; } function isSubStyleEqual( diff --git a/src/types/MarkdownStyleInternal.ts b/src/types/MarkdownStyleInternal.ts index a16ef693..8cb52ae0 100644 --- a/src/types/MarkdownStyleInternal.ts +++ b/src/types/MarkdownStyleInternal.ts @@ -137,11 +137,19 @@ interface InlineMathStyleInternal { color: string; } +interface SpoilerParticlesStyleInternal { + density: number; + speed: number; +} + +interface SpoilerSolidStyleInternal { + borderRadius: number; +} + interface SpoilerStyleInternal { color: string; - particleDensity: number; - particleSpeed: number; - solidBorderRadius: number; + particles: SpoilerParticlesStyleInternal; + solid: SpoilerSolidStyleInternal; } export interface MarkdownStyleInternal {