Skip to content

Commit 9531816

Browse files
Merge branch 'main' into @kacperzolkiewski/text-alignment-web
2 parents 52b52c8 + ffd9b39 commit 9531816

10 files changed

Lines changed: 2963 additions & 3472 deletions

File tree

.playwright/tests/strictMarks.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,30 @@ test.describe('strict marks', () => {
143143
await expect(boldBtn).toHaveClass(/toolbar-btn--active/);
144144
});
145145

146+
test('pressing Enter in the middle of bold that is followed by plain text keeps bold and types bold', async ({
147+
page,
148+
}) => {
149+
await setEditorHtml(page, '<html><p><b>hello</b> world</p></html>');
150+
151+
const editor = editorLocator(page);
152+
const boldBtn = toolbarButton(page, 'bold');
153+
154+
await editor.click();
155+
await editor.press('End');
156+
// land in he|llo
157+
for (let i = 0; i < 9; i++) {
158+
await editor.press('ArrowLeft', { delay: 80 });
159+
}
160+
161+
await editor.press('Enter');
162+
await expect(boldBtn).toHaveClass(/toolbar-btn--active/);
163+
164+
await editor.pressSequentially('X', { delay: 80 });
165+
await expect(boldBtn).toHaveClass(/toolbar-btn--active/);
166+
167+
await expect.poll(async () => getSerializedHtml(page)).toMatch(/<p><b>X/);
168+
});
169+
146170
test('pressing Enter after the last bold character keeps bold when the rest of the line is plain', async ({
147171
page,
148172
}) => {

apps/example/ios/Podfile.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2026,7 +2026,7 @@ EXTERNAL SOURCES:
20262026

20272027
SPEC CHECKSUMS:
20282028
FBLazyVector: c00c20551d40126351a6783c47ce75f5b374851b
2029-
hermes-engine: 484c595d9e6a0b7b7607e8ead508ba5c472493c7
2029+
hermes-engine: b4dad6ba67535bb03c8ff1006b337cba14db16cb
20302030
RCTDeprecation: 3bb167081b134461cfeb875ff7ae1945f8635257
20312031
RCTRequired: 74839f55d5058a133a0bc4569b0afec750957f64
20322032
RCTSwiftUI: 87a316382f3eab4dd13d2a0d0fd2adcce917361a
@@ -2035,7 +2035,7 @@ SPEC CHECKSUMS:
20352035
React: 1b1536b9099195944034e65b1830f463caaa8390
20362036
React-callinvoker: 6dff6d17d1d6cc8fdf85468a649bafed473c65f5
20372037
React-Core: 39ee05b5798296f433dd3c3624c57a187c1510e3
2038-
React-Core-prebuilt: 3ca7a49d919f940e7de8fb0c2a3f5cfcb665f09b
2038+
React-Core-prebuilt: 69556f895326f23c007f3a6869340045d7dca106
20392039
React-CoreModules: e78bfd2617075bc0e50c689df4a29232bd72ad82
20402040
React-cxxreact: 3fe21801d46097cf74c3dff6953677bebc4a3c2a
20412041
React-debug: e1f00fcd2cef58a2897471a6d76a4ef5f5f90c74
@@ -2097,8 +2097,8 @@ SPEC CHECKSUMS:
20972097
ReactAppDependencyProvider: 706b65371b90b5cc797b6639e8979f2e5cecd6da
20982098
ReactCodegen: ab01ebfffac5cda9140204eb872ed97c15df225f
20992099
ReactCommon: 47ef95b0920948a0b54d7439f7452501eeeac071
2100-
ReactNativeDependencies: 652705a9bc92800d0b1e15177a61ba70d89d24dd
2101-
ReactNativeEnrichedHtml: 93722241410f2daaa8c20ce6bcfcf4666bfd9166
2100+
ReactNativeDependencies: 8a208df374583424130645685d86306befc275cf
2101+
ReactNativeEnrichedHtml: 7d90df4aced7f533c7bd15ac296879b214413361
21022102
Yoga: e83c3121d079541e69f3c5c623faaaf933fb5812
21032103

21042104
PODFILE CHECKSUM: 88c10840d02e9884b2dc3f457d5120f83ac3803b

ios/EnrichedTextInputView.mm

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1743,7 +1743,6 @@ - (void)anyTextMayHaveBeenModified {
17431743
[self tryUpdatingHeight];
17441744
// update active styles as well
17451745
[self tryUpdatingActiveStyles];
1746-
[self layoutAttachments];
17471746
// update drawing - schedule debounced relayout
17481747
[self scheduleRelayoutIfNeeded];
17491748
}
@@ -1783,6 +1782,9 @@ - (void)_performRelayout {
17831782
// measureSize may not be up-to date at that point
17841783
CGSize measuredSize = [self measureSize:self->textView.frame.size.width];
17851784
self->textView.contentSize = measuredSize;
1785+
1786+
// Re-position attachment image views after the forced full re-layout
1787+
[self layoutAttachments];
17861788
});
17871789
}
17881790

ios/extensions/LayoutManagerExtension.mm

Lines changed: 73 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -274,69 +274,77 @@ - (void)drawLists:(id<EnrichedViewHost>)host
274274
[self glyphRangeForCharacterRange:[paragraph rangeValue]
275275
actualCharacterRange:nullptr];
276276

277-
[self enumerateLineFragmentsForGlyphRange:paragraphGlyphRange
278-
usingBlock:^(CGRect rect, CGRect usedRect,
279-
NSTextContainer *container,
280-
NSRange lineGlyphRange,
281-
BOOL *stop) {
282-
NSUInteger charIdx =
283-
[self characterIndexForGlyphAtIndex:
284-
lineGlyphRange.location];
285-
UIFont *font = [host.textView.textStorage
286-
attribute:NSFontAttributeName
287-
atIndex:charIdx
288-
effectiveRange:nil];
289-
CGRect textUsedRect =
290-
[self getTextAlignedUsedRect:usedRect
291-
font:font];
292-
293-
for (NSTextList *list in pStyle
294-
.textLists) {
295-
NSString *markerFormat =
296-
list.markerFormat;
297-
298-
if ([markerFormat
299-
hasPrefix:
300-
@"EnrichedAlignment"]) {
301-
continue;
302-
}
303-
304-
if ([markerFormat
305-
isEqualToString:
306-
@"EnrichedOrderedList"]) {
307-
NSString *marker = [self
308-
getDecimalMarkerForList:host
309-
charIndex:charIdx];
310-
[self drawDecimal:host
311-
marker:marker
312-
markerAttributes:markerAttributes
313-
origin:origin
314-
usedRect:usedRect
315-
indent:indent];
316-
} else if ([markerFormat
317-
isEqualToString:
318-
@"EnrichedUnordered"
319-
@"Lis"
320-
@"t"]) {
321-
[self drawBullet:host
322-
origin:origin
323-
usedRect:textUsedRect
324-
indent:indent];
277+
[self
278+
enumerateLineFragmentsForGlyphRange:paragraphGlyphRange
279+
usingBlock:^(CGRect rect, CGRect usedRect,
280+
NSTextContainer *container,
281+
NSRange lineGlyphRange,
282+
BOOL *stop) {
283+
NSUInteger charIdx =
284+
[self characterIndexForGlyphAtIndex:
285+
lineGlyphRange.location];
286+
UIFont *font = [host.textView.textStorage
287+
attribute:NSFontAttributeName
288+
atIndex:charIdx
289+
effectiveRange:nil];
290+
CGRect textUsedRect =
291+
[self getTextAlignedUsedRect:usedRect
292+
font:font];
293+
294+
for (NSTextList *list in pStyle
295+
.textLists) {
296+
NSString *markerFormat =
297+
list.markerFormat;
298+
299+
if ([markerFormat
300+
hasPrefix:
301+
@"EnrichedAlignment"]) {
302+
continue;
303+
}
325304

326-
} else if ([markerFormat
327-
hasPrefix:@"EnrichedChe"
328-
@"ckbox"]) {
329-
[self drawCheckbox:host
330-
markerFormat:markerFormat
305+
if ([markerFormat
306+
isEqualToString:
307+
@"EnrichedOrderedList"]) {
308+
CGPoint glyphLocation =
309+
[self locationForGlyphAtIndex:
310+
lineGlyphRange.location];
311+
CGFloat absoluteBaselineY =
312+
origin.y + rect.origin.y +
313+
glyphLocation.y;
314+
NSString *marker = [self
315+
getDecimalMarkerForList:host
316+
charIndex:charIdx];
317+
318+
[self drawDecimal:host
319+
marker:marker
320+
markerAttributes:markerAttributes
331321
origin:origin
332-
usedRect:textUsedRect
322+
baselineY:absoluteBaselineY
333323
indent:indent];
334-
}
324+
} else if ([markerFormat
325+
isEqualToString:
326+
@"EnrichedUnordered"
327+
@"Lis"
328+
@"t"]) {
329+
[self drawBullet:host
330+
origin:origin
331+
usedRect:textUsedRect
332+
indent:indent];
333+
334+
} else if ([markerFormat
335+
hasPrefix:@"EnrichedChe"
336+
@"ckbox"]) {
337+
[self drawCheckbox:host
338+
markerFormat:markerFormat
339+
origin:origin
340+
usedRect:textUsedRect
341+
indent:indent];
335342
}
336-
// only first line of a list gets its
337-
// marker drawn
338-
*stop = YES;
339-
}];
343+
}
344+
// only first line of a list gets its
345+
// marker drawn
346+
*stop = YES;
347+
}];
340348
}
341349
}
342350
}
@@ -439,13 +447,16 @@ - (void)drawDecimal:(id<EnrichedViewHost>)host
439447
marker:(NSString *)marker
440448
markerAttributes:(NSDictionary *)markerAttributes
441449
origin:(CGPoint)origin
442-
usedRect:(CGRect)usedRect
450+
baselineY:(CGFloat)baselineY
443451
indent:(CGFloat)indent {
444452
CGFloat gapWidth = [host.config orderedListGapWidth];
445453
CGSize markerSize = [marker sizeWithAttributes:markerAttributes];
446454
CGFloat markerX = origin.x + indent - gapWidth - markerSize.width / 2;
447-
CGFloat centerY = CGRectGetMidY(usedRect) + origin.y;
448-
CGFloat markerY = centerY - markerSize.height / 2.0;
455+
UIFont *markerFont = markerAttributes[NSFontAttributeName];
456+
457+
// drawAtPoint draws from the top-left bounding box of the string.
458+
// (baselineY - ascender) exactly pinpoints the top edge of the text
459+
CGFloat markerY = baselineY - markerFont.ascender;
449460

450461
[marker drawAtPoint:CGPointMake(markerX, markerY)
451462
withAttributes:markerAttributes];

ios/extensions/StringExtension.mm

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,81 @@ + (NSDictionary *)getEscapedCharactersInfoFrom:(NSString *)text {
5151
}
5252
}
5353

54+
// Numeric character references: &#NNNN; (decimal) and &#xHHHH; (hex)
55+
NSRegularExpression *numericEntityRegex = [NSRegularExpression
56+
regularExpressionWithPattern:@"&#([xX][0-9a-fA-F]+|[0-9]+);"
57+
options:0
58+
error:nil];
59+
60+
[numericEntityRegex
61+
enumerateMatchesInString:text
62+
options:0
63+
range:NSMakeRange(0, text.length)
64+
usingBlock:^(NSTextCheckingResult *match,
65+
NSMatchingFlags flags, BOOL *stop) {
66+
if (match == nil) {
67+
return;
68+
}
69+
NSRange fullRange = [match range];
70+
NSString *entityStr = [text substringWithRange:fullRange];
71+
NSString *valueStr =
72+
[text substringWithRange:[match rangeAtIndex:1]];
73+
74+
// Convert the matched string into a raw integer (UTF32
75+
// Code Point)
76+
UTF32Char codePoint = 0;
77+
if ([valueStr hasPrefix:@"x"] ||
78+
[valueStr hasPrefix:@"X"]) {
79+
// Parse Hexadecimal (base 16)
80+
const char *hexStr =
81+
[[valueStr substringFromIndex:1] UTF8String];
82+
codePoint = (UTF32Char)strtoul(hexStr, NULL, 16);
83+
} else {
84+
// Parse Decimal (base 10)
85+
const char *decStr = [valueStr UTF8String];
86+
codePoint = (UTF32Char)strtoul(decStr, NULL, 10);
87+
}
88+
89+
// Safety check: HTML numeric character references should
90+
// map to valid Unicode scalar values (0x0..0x10FFFF),
91+
// excluding surrogate code points (0xD800-0xDFFF). Per
92+
// HTML5, code point 0 is treated as U+FFFD. Replace
93+
// invalid values with U+FFFD (Replacement Character) to
94+
// avoid crashes/truncation.
95+
if (codePoint == 0 || codePoint > 0x10FFFF ||
96+
(codePoint >= 0xD800 && codePoint <= 0xDFFF)) {
97+
codePoint = 0xFFFD;
98+
}
99+
100+
NSString *decoded;
101+
if (codePoint <= 0xFFFF) {
102+
// STANDARD CHARACTER: Fits perfectly in one 16-bit
103+
// unichar.
104+
unichar ch = (unichar)codePoint;
105+
decoded = [NSString stringWithCharacters:&ch length:1];
106+
} else {
107+
// LARGE CHARACTER: Too big for 16 bits.
108+
// We must split the code point into two 16-bit halves
109+
// (a "Surrogate Pair") so NSString can store it
110+
// properly in UTF-16.
111+
UniChar surrogate[2];
112+
113+
// Calculate the "High" surrogate half
114+
surrogate[0] =
115+
(UniChar)(0xD800 + ((codePoint - 0x10000) >> 10));
116+
117+
// Calculate the "Low" surrogate half
118+
surrogate[1] =
119+
(UniChar)(0xDC00 + ((codePoint - 0x10000) & 0x3FF));
120+
121+
// Create the string using both 16-bit pieces
122+
decoded = [NSString stringWithCharacters:surrogate
123+
length:2];
124+
}
125+
126+
results[@(fullRange.location)] = @[ entityStr, decoded ];
127+
}];
128+
54129
return results;
55130
}
56131

ios/htmlParser/HtmlParser.mm

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,10 @@ + (NSArray *_Nonnull)getTextAndStylesFromHtml:(NSString *_Nonnull)fixedHtml {
645645

646646
// process tags into proper StyleType + StylePair values
647647
NSMutableArray *processedStyles = [[NSMutableArray alloc] init];
648+
// Tracks the number of processed images to remove their pre-generated
649+
// placeholder offsets from tag ranges when reading from plainText
650+
// (which does not contain those placeholders).
651+
NSInteger secondPassImageCount = 0;
648652

649653
for (NSArray *arr in initiallyProcessedTags) {
650654
NSString *tagName = (NSString *)arr[0];
@@ -715,6 +719,7 @@ + (NSArray *_Nonnull)getTextAndStylesFromHtml:(NSString *_Nonnull)fixedHtml {
715719
}
716720

717721
stylePair.styleValue = imageData;
722+
secondPassImageCount++;
718723
} else if ([tagName isEqualToString:@"u"]) {
719724
[styleArr addObject:@([UnderlineStyle getType])];
720725
} else if ([tagName isEqualToString:@"s"]) {
@@ -742,7 +747,14 @@ + (NSArray *_Nonnull)getTextAndStylesFromHtml:(NSString *_Nonnull)fixedHtml {
742747
NSString *url =
743748
[params substringWithRange:NSMakeRange(hrefRange.location + 6,
744749
hrefRange.length - 7)];
745-
NSString *text = [plainText substringWithRange:tagRangeValue.rangeValue];
750+
751+
// tagRange location includes one extra offset per preceding image
752+
// placeholder, which don't exist in plainText. Subtract them to map
753+
// back to the correct plainText index.
754+
NSRange adjustedRange = tagRangeValue.rangeValue;
755+
NSRange plainTextRange = NSMakeRange(
756+
adjustedRange.location - secondPassImageCount, adjustedRange.length);
757+
NSString *text = [plainText substringWithRange:plainTextRange];
746758

747759
LinkData *linkData = [[LinkData alloc] init];
748760
linkData.url = url;

package.json

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -88,25 +88,25 @@
8888
"registry": "https://registry.npmjs.org/"
8989
},
9090
"dependencies": {
91-
"@tiptap/core": "3.20.4",
92-
"@tiptap/extension-blockquote": "3.20.4",
93-
"@tiptap/extension-bold": "3.20.4",
94-
"@tiptap/extension-code": "3.20.4",
95-
"@tiptap/extension-document": "3.20.4",
96-
"@tiptap/extension-heading": "3.20.4",
97-
"@tiptap/extension-history": "3.20.4",
98-
"@tiptap/extension-image": "3.20.4",
99-
"@tiptap/extension-italic": "3.20.4",
100-
"@tiptap/extension-link": "3.20.4",
101-
"@tiptap/extension-list": "3.20.4",
102-
"@tiptap/extension-paragraph": "3.20.4",
103-
"@tiptap/extension-strike": "3.20.4",
104-
"@tiptap/extension-text": "3.20.4",
105-
"@tiptap/extension-text-align": "3.20.4",
106-
"@tiptap/extension-underline": "3.20.4",
107-
"@tiptap/extensions": "3.20.4",
108-
"@tiptap/pm": "3.20.4",
109-
"@tiptap/react": "3.20.4"
91+
"@tiptap/core": "3.27.1",
92+
"@tiptap/extension-blockquote": "3.27.1",
93+
"@tiptap/extension-bold": "3.27.1",
94+
"@tiptap/extension-code": "3.27.1",
95+
"@tiptap/extension-document": "3.27.1",
96+
"@tiptap/extension-heading": "3.27.1",
97+
"@tiptap/extension-history": "3.27.1",
98+
"@tiptap/extension-image": "3.27.1",
99+
"@tiptap/extension-italic": "3.27.1",
100+
"@tiptap/extension-link": "3.27.1",
101+
"@tiptap/extension-list": "3.27.1",
102+
"@tiptap/extension-paragraph": "3.27.1",
103+
"@tiptap/extension-strike": "3.27.1",
104+
"@tiptap/extension-text": "3.27.1",
105+
"@tiptap/extension-text-align": "3.27.1",
106+
"@tiptap/extension-underline": "3.27.1",
107+
"@tiptap/extensions": "3.27.1",
108+
"@tiptap/pm": "3.27.1",
109+
"@tiptap/react": "3.27.1"
110110
},
111111
"devDependencies": {
112112
"@commitlint/config-conventional": "^19.6.0",

0 commit comments

Comments
 (0)