Skip to content

Commit 97e269d

Browse files
mozharovskyclaude
andcommitted
feat: add list.markerWidth for aligning unordered, ordered, and task lists
Today each list type reserves its own natural marker column width — the bullet radius (`bulletSize / 2`) for unordered, the width of `"99."` at the marker font for ordered, and the checkbox size for tasks. Mixed lists therefore look ragged: bullets and task boxes hang far to the left of numbers. The new optional `list.markerWidth` acts as a floor applied to all three list types. Each list's effective column becomes `max(markerWidth, natural)`, so consumers can widen the gutter uniformly without shrinking ordered lists or resizing bullet/checkbox glyphs. Values below the natural width are ignored. ```tsx <EnrichedMarkdownText markdownStyle={{ list: { bulletSize: 6, markerWidth: 22, // widens UL and task rows to match OL at 17pt }, }} ... /> ``` Implementation: - Public `ListStyle.markerWidth?: number` (undefined = current per-list-natural behavior). - Internal codegen prop is a concrete `Float`; negative = "auto". Default is set in `normalizeMarkdownStyle` so consumers never touch the sentinel. - iOS: `StyleConfig` now floors each list kind — new `effectiveListMarginLeftForTask` plus updated `effectiveListMarginLeftForBullet` / `effectiveListMarginLeftForNumber`. `ListItemRenderer.m` routes the task case through the new accessor. Wired through `StylePropsUtils` and included in the measurement cache key so cached sizes invalidate when it changes. - Android: `ListStyle.effectiveMarkerWidth(natural)` helper; each span (`UnorderedListSpan`, `OrderedListSpan`, `TaskListSpan`) applies the floor. Bullets and checkboxes now position themselves relative to the reserved column's right edge so they line up flush with the gap — behaviorally identical at the default since the column width equals the natural glyph width. - Docs: replaced the old bulletSize-only row in `docs/STYLES.md` with the shared `markerWidth` semantics. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 86914a0 commit 97e269d

17 files changed

Lines changed: 80 additions & 12 deletions

android/src/main/java/com/swmansion/enriched/markdown/spans/OrderedListSpan.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class OrderedListSpan(
5252

5353
override fun getMarkerWidth(): Float {
5454
val paint = configureMarkerPaint()
55-
return paint.measureText("99.")
55+
return listStyle.effectiveMarkerWidth(paint.measureText("99."))
5656
}
5757

5858
var itemNumber: Int = 1

android/src/main/java/com/swmansion/enriched/markdown/spans/TaskListSpan.kt

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import com.swmansion.enriched.markdown.styles.TaskListStyle
1313

1414
class TaskListSpan(
1515
private val taskStyle: TaskListStyle,
16-
listStyle: ListStyle,
16+
private val listStyle: ListStyle,
1717
depth: Int,
1818
context: Context,
1919
styleCache: SpanStyleCache,
@@ -34,6 +34,7 @@ class TaskListSpan(
3434
gapWidth = listStyle.gapWidth,
3535
) {
3636
private val checkboxSize = taskStyle.checkboxSize
37+
private val markerColumnWidth = listStyle.effectiveMarkerWidth(checkboxSize)
3738
private val cornerRadius = taskStyle.checkboxBorderRadius
3839
private val rect = RectF()
3940
private val checkPath = Path()
@@ -55,7 +56,7 @@ class TaskListSpan(
5556
strokeJoin = Paint.Join.ROUND
5657
}
5758

58-
override fun getMarkerWidth(): Float = checkboxSize
59+
override fun getMarkerWidth(): Float = markerColumnWidth
5960

6061
override fun drawMarker(
6162
canvas: Canvas,
@@ -71,7 +72,10 @@ class TaskListSpan(
7172
val fontMetrics = paint.fontMetrics
7273
val capHeight = -fontMetrics.ascent * CAP_HEIGHT_RATIO
7374
val centerY = baseline - capHeight / HALF_DIVISOR
74-
val centerX = x + (depth * marginLeft + checkboxSize / HALF_DIVISOR) * dir
75+
// Right-align the checkbox inside the reserved marker column so it hugs
76+
// the gap before the text. At the default (markerColumnWidth ==
77+
// checkboxSize) this is identical to the previous flush-left layout.
78+
val centerX = x + (depth * marginLeft + markerColumnWidth - checkboxSize / HALF_DIVISOR) * dir
7579
val half = checkboxSize / HALF_DIVISOR
7680
rect.set(centerX - half, centerY - half, centerX + half, centerY + half)
7781

android/src/main/java/com/swmansion/enriched/markdown/spans/UnorderedListSpan.kt

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,14 @@ class UnorderedListSpan(
3535
}
3636

3737
private val radius: Float = listStyle.bulletSize / 2f
38+
private val markerColumnWidth: Float = listStyle.effectiveMarkerWidth(radius)
3839

3940
private fun configureBulletPaint(): Paint =
4041
sharedBulletPaint.apply {
4142
color = listStyle.bulletColor
4243
}
4344

44-
override fun getMarkerWidth(): Float = radius
45+
override fun getMarkerWidth(): Float = markerColumnWidth
4546

4647
override fun drawMarker(
4748
canvas: Canvas,
@@ -55,7 +56,11 @@ class UnorderedListSpan(
5556
start: Int,
5657
) {
5758
val bulletPaint = configureBulletPaint()
58-
val bulletX = x + (depth * marginLeft + radius) * dir
59+
// Center the bullet at the right edge of the reserved marker column so
60+
// it hugs the gap before the text — matches iOS behavior and stays
61+
// visually flush-left when the column width equals the bullet radius
62+
// (the default).
63+
val bulletX = x + (depth * marginLeft + markerColumnWidth) * dir
5964
val fontMetrics = paint.fontMetrics
6065
val bulletY = baseline + (fontMetrics.ascent + fontMetrics.descent) / 2f
6166

android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,17 @@ data class ListStyle(
1212
override val lineHeight: Float,
1313
val bulletColor: Int,
1414
val bulletSize: Float,
15+
val markerWidth: Float,
1516
val markerColor: Int,
1617
val markerFontWeight: String,
1718
val gapWidth: Float,
1819
val marginLeft: Float,
1920
) : BaseBlockStyle {
21+
22+
/** Floor `naturalWidth` by the consumer-configured `markerWidth`. */
23+
fun effectiveMarkerWidth(naturalWidth: Float): Float =
24+
if (markerWidth > naturalWidth) markerWidth else naturalWidth
25+
2026
companion object {
2127
fun fromReadableMap(
2228
map: ReadableMap,
@@ -32,6 +38,13 @@ data class ListStyle(
3238
val lineHeight = parser.toPixelFromSP(lineHeightRaw)
3339
val bulletColor = parser.parseColor(map, "bulletColor")
3440
val bulletSize = parser.toPixelFromDIP(map.getDouble("bulletSize").toFloat())
41+
val markerWidthRaw = map.getDouble("markerWidth").toFloat()
42+
val markerWidth =
43+
if (markerWidthRaw < 0f) {
44+
-1f
45+
} else {
46+
parser.toPixelFromDIP(markerWidthRaw)
47+
}
3548
val markerColor = parser.parseColor(map, "markerColor")
3649
val markerFontWeight = parser.parseString(map, "markerFontWeight", "normal")
3750
val gapWidth = parser.toPixelFromDIP(map.getDouble("gapWidth").toFloat())
@@ -47,6 +60,7 @@ data class ListStyle(
4760
lineHeight,
4861
bulletColor,
4962
bulletSize,
63+
markerWidth,
5064
markerColor,
5165
markerFontWeight,
5266
gapWidth,

apps/example/src/markdownStyles.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const customMarkdownStyle: MarkdownStyle = {
6969
lineHeight: Platform.select({ ios: 22, android: 26, default: 26 }),
7070
bulletColor: '#6b7280',
7171
bulletSize: 6,
72+
markerWidth: 20,
7273
markerColor: '#6b7280',
7374
markerFontWeight: '500',
7475
gapWidth: 8,

docs/STYLES.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ The library provides sensible default styles for all Markdown elements out of th
238238
|----------|------|-------------|
239239
| `bulletColor` | `string` | Bullet point color |
240240
| `bulletSize` | `number` | Bullet point size |
241+
| `markerWidth` | `number` | Minimum reserved marker column width (floors the natural width of every list type) |
241242
| `markerColor` | `string` | Number marker color |
242243
| `markerFontWeight` | `string` | Number marker font weight |
243244
| `gapWidth` | `number` | Gap between marker and text |

ios/internals/MeasurementCache.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ template <typename StyleStruct> inline size_t computeStyleFingerprint(const Styl
103103
hashFields(s.blockquote.borderWidth, s.blockquote.gapWidth);
104104

105105
hashTextLayout(s.list);
106-
hashFields(s.list.bulletSize, s.list.markerFontWeight, s.list.gapWidth, s.list.marginLeft);
106+
hashFields(s.list.bulletSize, s.list.markerWidth, s.list.markerFontWeight, s.list.gapWidth, s.list.marginLeft);
107107

108108
// Code & Inlines
109109
hashFields(s.codeBlock.fontFamily, s.codeBlock.fontSize, s.codeBlock.fontWeight, s.codeBlock.marginTop,

ios/renderer/ListItemRenderer.m

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ - (void)renderNode:(MarkdownASTNode *)node into:(NSMutableAttributedString *)out
7373

7474
// currentDepth - 1 handles the horizontal offset for nested lists
7575
const NSInteger nestingLevel = currentDepth - 1;
76-
const CGFloat baseMarkerWidth = isTask ? [_config taskListCheckboxSize]
76+
const CGFloat baseMarkerWidth = isTask ? [_config effectiveListMarginLeftForTask]
7777
: (context.listType == ListTypeOrdered) ? [_config effectiveListMarginLeftForNumber]
7878
: [_config effectiveListMarginLeftForBullet];
7979

ios/styles/StyleConfig.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,8 @@
236236
- (void)setListStyleBulletColor:(RCTUIColor *)newValue;
237237
- (CGFloat)listStyleBulletSize;
238238
- (void)setListStyleBulletSize:(CGFloat)newValue;
239+
- (CGFloat)listStyleMarkerWidth;
240+
- (void)setListStyleMarkerWidth:(CGFloat)newValue;
239241
- (RCTUIColor *)listStyleMarkerColor;
240242
- (void)setListStyleMarkerColor:(RCTUIColor *)newValue;
241243
- (NSString *)listStyleMarkerFontWeight;
@@ -249,6 +251,7 @@
249251
- (CGFloat)effectiveListGapWidth;
250252
- (CGFloat)effectiveListMarginLeftForBullet;
251253
- (CGFloat)effectiveListMarginLeftForNumber;
254+
- (CGFloat)effectiveListMarginLeftForTask;
252255
// Code block properties
253256
- (CGFloat)codeBlockFontSize;
254257
- (void)setCodeBlockFontSize:(CGFloat)newValue;

ios/styles/StyleConfig.mm

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,7 @@ @implementation StyleConfig {
151151
CGFloat _listStyleLineHeight;
152152
RCTUIColor *_listStyleBulletColor;
153153
CGFloat _listStyleBulletSize;
154+
CGFloat _listStyleMarkerWidth;
154155
RCTUIColor *_listStyleMarkerColor;
155156
NSString *_listStyleMarkerFontWeight;
156157
CGFloat _listStyleGapWidth;
@@ -434,6 +435,7 @@ - (id)copyWithZone:(NSZone *)zone
434435
copy->_listStyleLineHeight = _listStyleLineHeight;
435436
copy->_listStyleBulletColor = [_listStyleBulletColor copy];
436437
copy->_listStyleBulletSize = _listStyleBulletSize;
438+
copy->_listStyleMarkerWidth = _listStyleMarkerWidth;
437439
copy->_listStyleMarkerColor = [_listStyleMarkerColor copy];
438440
copy->_listStyleMarkerFontWeight = [_listStyleMarkerFontWeight copy];
439441
copy->_listStyleGapWidth = _listStyleGapWidth;
@@ -1706,6 +1708,16 @@ - (void)setListStyleBulletSize:(CGFloat)newValue
17061708
_listStyleBulletSize = newValue;
17071709
}
17081710

1711+
- (CGFloat)listStyleMarkerWidth
1712+
{
1713+
return _listStyleMarkerWidth;
1714+
}
1715+
1716+
- (void)setListStyleMarkerWidth:(CGFloat)newValue
1717+
{
1718+
_listStyleMarkerWidth = newValue;
1719+
}
1720+
17091721
- (RCTUIColor *)listStyleMarkerColor
17101722
{
17111723
return _listStyleMarkerColor;
@@ -1786,16 +1798,30 @@ - (CGFloat)effectiveListGapWidth
17861798

17871799
- (CGFloat)effectiveListMarginLeftForBullet
17881800
{
1789-
// Just the minimum width needed for bullet (radius)
1790-
return _listStyleBulletSize / 2.0;
1801+
// Natural width for a bullet is the bullet radius (bulletSize / 2). The
1802+
// consumer-provided `markerWidth` acts as a floor so bulleted lists can
1803+
// line up with ordered / task lists without resizing the bullet glyph.
1804+
CGFloat natural = _listStyleBulletSize / 2.0;
1805+
return _listStyleMarkerWidth > natural ? _listStyleMarkerWidth : natural;
17911806
}
17921807

17931808
- (CGFloat)effectiveListMarginLeftForNumber
17941809
{
1795-
// Reserve width for numbers up to 99 (matching Android)
1810+
// Reserve width for numbers up to 99 (matching Android). `markerWidth`
1811+
// floors the value so consumers can widen the column uniformly.
17961812
UIFont *font = [self listMarkerFont];
1797-
return
1813+
CGFloat natural =
17981814
[@"99." sizeWithAttributes:@{NSFontAttributeName : font ?: [UIFont systemFontOfSize:_listStyleFontSize]}].width;
1815+
return _listStyleMarkerWidth > natural ? _listStyleMarkerWidth : natural;
1816+
}
1817+
1818+
- (CGFloat)effectiveListMarginLeftForTask
1819+
{
1820+
// Task checkbox reserves at least its own size. `markerWidth` can widen
1821+
// the column so task rows align with adjacent UL/OL rows. The checkbox
1822+
// glyph itself is still drawn at its configured size.
1823+
CGFloat natural = [self taskListCheckboxSize];
1824+
return _listStyleMarkerWidth > natural ? _listStyleMarkerWidth : natural;
17991825
}
18001826

18011827
// Code block properties

0 commit comments

Comments
 (0)