iOS: ordered-list marker crashes the app when list.markerColor normalizes to null ('', var(--x), oklch(...), …)
Environment
|
|
| react-native-enriched-markdown |
1.1.0-nightly-20260921-971111abf (we originally hit this on 1.0.2 stable) |
| react-native |
0.86.3 |
| expo |
57.0.24 |
| react |
19.2.8 |
| Platform |
iOS (native crash; reproduced on iOS Simulator) |
Summary
If list.markerColor is a string that @react-native/normalize-colors cannot parse, it becomes null and is forwarded to the native side as-is. For ordered lists, the native marker drawer builds an NSDictionary literal with that value, so a nil color throws:
*** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[1]
The JS layer already added a default for the "prop omitted" case, but an explicitly provided unparseable value still reaches native. Since design-token pipelines commonly emit modern CSS color syntax (oklch(...), color-mix(...)), this is easy to hit in practice.
Steps to reproduce
import { EnrichedMarkdownText } from 'react-native-enriched-markdown';
<EnrichedMarkdownText
markdown={'1. first\n2. second'} // an ordered list is required
markdownStyle={{ list: { markerColor: 'var(--text-primary)' } }}
/>
Any of these values triggers it:
'' · 'var(--x)' · 'oklch(60% 0.2 250)' · 'color-mix(in oklab, red 50%, blue)' · 'lab(50% 40 59.5)' · 'color(display-p3 1 0 0)'
Actual
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',
reason: '*** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]:
attempt to insert nil object from objects[1]'
Expected
An unparseable color should be ignored (fall back to the default marker color) instead of crashing the app.
Root cause
ios/utils/ListMarkerDrawer.m:194
NSDictionary *mAttrs = @{NSFontAttributeName : font,
NSForegroundColorAttributeName : [_config listStyleMarkerColor]};
No nil guard: objects[1] is the color, so a nil color always throws. Note this is the only place in the library that reads _config listStyleMarkerColor through a dictionary literal.
Contrast with the bullet path in the same file (drawBulletAtX:centerY:depth:), which uses [[_config listStyleMarkerColor] setFill] — nil-tolerant, which is why unordered lists don't crash while ordered lists do.
On the JS side the default only covers the "not provided" case:
// src/normalizeMarkdownStyle.ts:162
markerColor: normalizeColor('#6B7280')!,
The ! is a tell: the value may be undefined/null, and it is forwarded regardless. Public typing doesn't prevent it either — markdownStyle.list.markerColor?: string — and the native prop is typed markerColor: ColorValue.
What normalizeColor actually accepts (measured, RN 0.86.3)
| input |
result |
#6B7280, #fff, rgba(0,0,0,0.5), rgb(1 2 3 / 0.5), red, transparent, hwb(120 0% 0%) |
valid ✓ |
'' |
null ✗ |
var(--x) |
null ✗ |
oklch(60% 0.2 250) |
null ✗ |
color-mix(in oklab, red 50%, blue) |
null ✗ |
lab(50% 40 59.5) |
null ✗ |
color(display-p3 1 0 0) |
null ✗ |
(measurement: require('@react-native/normalize-colors') called with each of these strings)
This is the part that makes the bug non-edge-case: Tailwind/uniwind-style themes are defined with oklch() and generate color-mix() derivatives, so a token read straight from the theme is a perfectly valid CSS color that this pipeline turns into a native crash.
Suggested fix
-
Native (covers every input)
UIColor *markerColor = [_config listStyleMarkerColor] ?: UIColor.labelColor;
NSDictionary *mAttrs = @{NSFontAttributeName : font,
NSForegroundColorAttributeName : markerColor};
(or insert conditionally via an NSMutableDictionary)
-
JS (fail fast at the boundary) — resolve the fallback inside the internal style object instead of forwarding null to native.
-
Worth auditing — ios/input/ENRMInputListMarkerDrawer.mm:112,124 uses the same dictionary shape with a different value source; same class of risk.
Workaround on our side
We only pass colors that normalizeColor can parse (design tokens resolved to hex/rgb) and never pass '' or var(--x). It works, but it's a footgun: oklch(...) is a legitimate CSS color that a design system will hand you without warning.
Additional context
- Only the ordered list path crashes (bullets are safe), so the markdown body needs at least one OL item.
- Web is unaffected —
normalizeMarkdownStyle.web.ts doesn't normalize, the string goes straight to CSS.
- Happy to open a PR with the native nil-coalesce above if that's preferred.
iOS: ordered-list marker crashes the app when
list.markerColornormalizes tonull('',var(--x),oklch(...), …)Environment
1.1.0-nightly-20260921-971111abf(we originally hit this on1.0.2stable)Summary
If
list.markerColoris a string that@react-native/normalize-colorscannot parse, it becomesnulland is forwarded to the native side as-is. For ordered lists, the native marker drawer builds anNSDictionaryliteral with that value, so anilcolor throws:*** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[1]The JS layer already added a default for the "prop omitted" case, but an explicitly provided unparseable value still reaches native. Since design-token pipelines commonly emit modern CSS color syntax (
oklch(...),color-mix(...)), this is easy to hit in practice.Steps to reproduce
Any of these values triggers it:
''·'var(--x)'·'oklch(60% 0.2 250)'·'color-mix(in oklab, red 50%, blue)'·'lab(50% 40 59.5)'·'color(display-p3 1 0 0)'Actual
Expected
An unparseable color should be ignored (fall back to the default marker color) instead of crashing the app.
Root cause
ios/utils/ListMarkerDrawer.m:194No nil guard:
objects[1]is the color, so anilcolor always throws. Note this is the only place in the library that reads_config listStyleMarkerColorthrough a dictionary literal.Contrast with the bullet path in the same file (
drawBulletAtX:centerY:depth:), which uses[[_config listStyleMarkerColor] setFill]— nil-tolerant, which is why unordered lists don't crash while ordered lists do.On the JS side the default only covers the "not provided" case:
The
!is a tell: the value may beundefined/null, and it is forwarded regardless. Public typing doesn't prevent it either —markdownStyle.list.markerColor?: string— and the native prop is typedmarkerColor: ColorValue.What
normalizeColoractually accepts (measured, RN 0.86.3)#6B7280,#fff,rgba(0,0,0,0.5),rgb(1 2 3 / 0.5),red,transparent,hwb(120 0% 0%)''null✗var(--x)null✗oklch(60% 0.2 250)null✗color-mix(in oklab, red 50%, blue)null✗lab(50% 40 59.5)null✗color(display-p3 1 0 0)null✗(measurement:
require('@react-native/normalize-colors')called with each of these strings)This is the part that makes the bug non-edge-case: Tailwind/uniwind-style themes are defined with
oklch()and generatecolor-mix()derivatives, so a token read straight from the theme is a perfectly valid CSS color that this pipeline turns into a native crash.Suggested fix
Native (covers every input)
(or insert conditionally via an
NSMutableDictionary)JS (fail fast at the boundary) — resolve the fallback inside the internal style object instead of forwarding
nullto native.Worth auditing —
ios/input/ENRMInputListMarkerDrawer.mm:112,124uses the same dictionary shape with a different value source; same class of risk.Workaround on our side
We only pass colors that
normalizeColorcan parse (design tokens resolved to hex/rgb) and never pass''orvar(--x). It works, but it's a footgun:oklch(...)is a legitimate CSS color that a design system will hand you without warning.Additional context
normalizeMarkdownStyle.web.tsdoesn't normalize, the string goes straight to CSS.