Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)
}
}
}
2 changes: 1 addition & 1 deletion docs/STYLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|----------|------|-------------|
Expand Down
3 changes: 0 additions & 3 deletions ios/styles/StyleConfig.mm
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,6 @@ - (instancetype)init
_tableFontNeedsRecreation = YES;
_tableHeaderFontNeedsRecreation = YES;
_linkUnderline = YES;
_spoilerParticleDensity = 8.0;
_spoilerParticleSpeed = 20.0;
_spoilerSolidBorderRadius = 4.0;
return self;
}

Expand Down
12 changes: 6 additions & 6 deletions ios/utils/StylePropsUtils.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
14 changes: 11 additions & 3 deletions src/EnrichedMarkdownNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
14 changes: 11 additions & 3 deletions src/EnrichedMarkdownTextNativeComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 1 addition & 3 deletions src/normalizeMarkdownInputStyle.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { processColor, type ColorValue } from 'react-native';
import type { MarkdownInputStyle } from './EnrichedMarkdownInput';
import { normalizeColor } from './styleUtils';

interface MarkdownInputStyleInternal {
strong: {
Expand All @@ -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';
Expand Down
53 changes: 9 additions & 44 deletions src/normalizeMarkdownStyle.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -37,25 +23,6 @@ const getMonospaceFont = (): string =>
default: 'monospace',
})!;

function mergeSubStyle<T extends Record<string, unknown>>(
defaultStyle: T,
userStyle?: Partial<T>
): T {
if (!userStyle) return defaultStyle;
const result: Record<string, unknown> = { ...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')!;

Expand All @@ -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(),
Expand Down Expand Up @@ -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<MarkdownStyle, MarkdownStyleInternal>();
const structuralCache: {
Expand Down Expand Up @@ -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<string, unknown> | undefined);
const userValue = style[key] as unknown as
| Record<string, unknown>
| undefined;
result[key] = mergeSubStyle(
DEFAULT_NORMALIZED_STYLE[key] as unknown as Record<string, unknown>,
userValue as Record<string, unknown> | undefined
Expand Down
22 changes: 6 additions & 16 deletions src/normalizeMarkdownStyle.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends Record<string, unknown>>(
defaultStyle: T,
userStyle?: Partial<T>
): T {
if (!userStyle) return defaultStyle;
return { ...defaultStyle, ...userStyle };
}

const defaultTextColor = '#1F2937';
const defaultHeadingColor = '#111827';

Expand Down Expand Up @@ -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 },
},
});

Expand Down Expand Up @@ -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<string, unknown> | undefined);
const userValue = style[key] as unknown as
| Record<string, unknown>
| undefined;
result[key] = mergeSubStyle(
DEFAULT_NORMALIZED_STYLE[key] as unknown as Record<string, unknown>,
userValue as Record<string, unknown> | undefined
Expand Down
55 changes: 39 additions & 16 deletions src/styleUtils.ts
Original file line number Diff line number Diff line change
@@ -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<MarkdownStyleInternal['spoiler']> | undefined {
if (!userSpoiler) return undefined;
const flat: Record<string, unknown> = {};
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<MarkdownStyleInternal['spoiler']>)
: 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<T extends Record<string, unknown>>(
defaultStyle: T,
userStyle?: Partial<T>
): T {
if (!userStyle) return defaultStyle;
const result: Record<string, unknown> = { ...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<string, unknown>),
...(userValue as Record<string, unknown>),
};
}
if (
key.toLowerCase().includes('color') &&
typeof result[key] === 'string'
) {
result[key] = normalizeColor(result[key] as string);
}
}
return result as T;
}

function isSubStyleEqual(
Expand Down
14 changes: 11 additions & 3 deletions src/types/MarkdownStyleInternal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading