Skip to content

Commit 02eebe0

Browse files
authored
Merge branch '@ksienkiewicz/docs-core-functionalities' into @ksienkiewicz/docs-guides
2 parents a11e375 + 01244ae commit 02eebe0

28 files changed

Lines changed: 266 additions & 269 deletions

File tree

android/src/main/java/com/swmansion/enriched/text/MeasurementStore.kt

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import com.facebook.react.views.text.ReactTypefaceUtils.parseFontWeight
1717
import com.facebook.yoga.YogaMeasureMode
1818
import com.facebook.yoga.YogaMeasureOutput
1919
import com.swmansion.enriched.common.EnrichedConstants
20+
import com.swmansion.enriched.common.GumboNormalizer
2021
import com.swmansion.enriched.common.allowFontScalingFromProps
2122
import com.swmansion.enriched.common.parser.EnrichedParser
2223
import com.swmansion.enriched.common.pixelFromSpOrDp
@@ -104,24 +105,35 @@ object MeasurementStore {
104105
props: ReadableMap?,
105106
): CharSequence {
106107
val text = props?.getString("text") ?: ""
108+
val isInternalHtml = text.startsWith("<html>") && text.endsWith("</html>")
109+
val useHtmlNormalizer = useHtmlNormalizerFromProps(props)
107110

108-
val isHtml = text.startsWith("<html>") && text.endsWith("</html>")
109-
if (!isHtml) return text
111+
if (!isInternalHtml && !useHtmlNormalizer) {
112+
return text
113+
}
110114

111115
try {
116+
val textToParse = if (isInternalHtml) text else GumboNormalizer.normalizeHtml(text)
112117
val style = props?.getMap("htmlStyle") ?: return text
113118
val allowFontScaling = allowFontScalingFromProps(props)
114-
val enrichedStyle =
115-
EnrichedTextStyle.fromReadableMap(context as ReactContext, fontSize, style, allowFontScaling)
119+
val enrichedStyle = EnrichedTextStyle.fromReadableMap(context as ReactContext, fontSize, style, allowFontScaling)
120+
116121
val factory = EnrichedTextSpanFactory()
117-
val parsed = EnrichedParser.fromHtml(text, enrichedStyle, factory)
122+
val parsed = EnrichedParser.fromHtml(textToParse, enrichedStyle, factory)
118123
return parsed.trimEnd('\n')
119124
} catch (e: Exception) {
120125
Log.w("MeasurementStore", "Error parsing initial HTML text: ${e.message}")
121126
return text
122127
}
123128
}
124129

130+
private fun useHtmlNormalizerFromProps(props: ReadableMap?): Boolean {
131+
if (props == null || !props.hasKey("useHtmlNormalizer") || props.isNull("useHtmlNormalizer")) {
132+
return false
133+
}
134+
return props.getBoolean("useHtmlNormalizer")
135+
}
136+
125137
private fun getInitialFontSize(props: ReadableMap?): Float {
126138
val propsFontSize = props?.getDouble("fontSize")?.toFloat() ?: EnrichedConstants.TEXT_DEFAULT_FONT_SIZE
127139
val fontSize =

android/src/main/new_arch/react/renderer/components/ReactNativeEnrichedSpec/conversions.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ inline folly::dynamic toDynamic(const EnrichedTextInputViewProps &props) {
1919
serializedProps["fontFamily"] = props.fontFamily;
2020
serializedProps["lineHeight"] = props.lineHeight;
2121
serializedProps["allowFontScaling"] = props.allowFontScaling;
22+
serializedProps["useHtmlNormalizer"] = props.useHtmlNormalizer;
2223
serializedProps["htmlStyle"] = toDynamic(props.htmlStyle);
2324

2425
return serializedProps;
@@ -36,6 +37,7 @@ inline folly::dynamic toDynamic(const EnrichedTextViewProps &props) {
3637
serializedProps["numberOfLines"] = props.numberOfLines;
3738
serializedProps["ellipsizeMode"] = props.ellipsizeMode;
3839
serializedProps["allowFontScaling"] = props.allowFontScaling;
40+
serializedProps["useHtmlNormalizer"] = props.useHtmlNormalizer;
3941
serializedProps["htmlStyle"] = toDynamic(props.htmlStyle);
4042

4143
return serializedProps;

cpp/parser/GumboNormalizer.c

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -548,17 +548,34 @@ static void walk_node(GumboNode *node, buffer_t *out);
548548

549549
static void flatten_bq_node(GumboNode *node, buffer_t *ib, buffer_t *out);
550550

551-
static void flush_inline_p(buffer_t *ib, buffer_t *out,
551+
/** True if buf is empty or contains only ASCII whitespace. */
552+
static bool is_whitespace_only(const char *data, size_t len) {
553+
for (size_t i = 0; i < len; i++) {
554+
unsigned char c = (unsigned char)data[i];
555+
if (c != ' ' && c != '\t' && c != '\n' && c != '\r' && c != '\f')
556+
return false;
557+
}
558+
return true;
559+
}
560+
561+
/**
562+
* Flush buffered inline content as a <p>. Inter-block whitespace (newlines /
563+
* spaces between block tags in pretty-printed HTML) is discarded so it does
564+
* not become empty paragraphs that later serialize as extra <br>s.
565+
*/
566+
static bool flush_inline_p(buffer_t *ib, buffer_t *out,
552567
GumboElement *align_el) {
553-
if (ib->len > 0) {
568+
bool emitted = ib->len > 0 && !is_whitespace_only(ib->data, ib->len);
569+
if (emitted) {
554570
buffer_append_str(out, "<p");
555571
if (align_el)
556572
emit_alignment(align_el, "p", out);
557573
buffer_append_str(out, ">");
558574
buffer_append(out, ib->data, ib->len);
559575
buffer_append_str(out, "</p>");
560-
buffer_clear(ib);
561576
}
577+
buffer_clear(ib);
578+
return emitted;
562579
}
563580

564581
static void flatten_bq_children(GumboNode *node, buffer_t *ib, buffer_t *out) {
@@ -730,9 +747,8 @@ static void walk_children(GumboNode *node, buffer_t *out) {
730747
!is_blockquote_node(children->data[i])) {
731748
child = children->data[i];
732749
if (is_br_node(child)) {
733-
if (ib.len > 0)
734-
flush_inline_p(&ib, out, NULL);
735-
else
750+
/* Whitespace-only buffer is layout noise; treat like empty → <br> */
751+
if (!flush_inline_p(&ib, out, NULL))
736752
buffer_append_str(out, "<br>");
737753
i++;
738754
continue;

cpp/tests/GumboParserTest.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,3 +571,30 @@ TEST(GumboParserTest, TextAlignment) {
571571
"<p style=\"text-align: center\">c</p>"
572572
"<p style=\"text-align: right\">r</p></blockquote>");
573573
}
574+
575+
TEST(GumboParserTest, InterBlockWhitespace) {
576+
// Pretty-printed consecutive paragraphs must not gain empty <p>s from the
577+
// newlines between them (those would later serialize as extra <br>s).
578+
EXPECT_EQ(GumboParser::normalizeHtml(
579+
"<p>Asdasd</p>\n<p>Asdasd</p>\n<p>Asdasda</p>"),
580+
"<p>Asdasd</p><p>Asdasd</p><p>Asdasda</p>");
581+
EXPECT_EQ(GumboParser::normalizeHtml(
582+
"<p>Asdasd</p>\n\n<p>Asdasd</p>\n\n<p>Asdasda</p>"),
583+
"<p>Asdasd</p><p>Asdasd</p><p>Asdasda</p>");
584+
EXPECT_EQ(GumboParser::normalizeHtml(
585+
"<html>\n<p>Asdasd</p>\n<p>Asdasd</p>\n<p>Asdasda</p>\n</html>"),
586+
"<p>Asdasd</p><p>Asdasd</p><p>Asdasda</p>");
587+
EXPECT_EQ(GumboParser::normalizeHtml("<p>Asdasd</p> <p>Asdasd</p>"),
588+
"<p>Asdasd</p><p>Asdasd</p>");
589+
590+
// Significant inline content between blocks is still wrapped in <p>.
591+
EXPECT_EQ(GumboParser::normalizeHtml("<p>a</p> hello <p>b</p>"),
592+
"<p>a</p><p> hello </p><p>b</p>");
593+
594+
// Spaces inside text / between inlines must be preserved.
595+
EXPECT_EQ(GumboParser::normalizeHtml("hello world"), "hello world");
596+
EXPECT_EQ(GumboParser::normalizeHtml("<p>hello world</p>"),
597+
"<p>hello world</p>");
598+
EXPECT_EQ(GumboParser::normalizeHtml("<b>hello</b> <i>world</i>"),
599+
"<b>hello</b> <i>world</i>");
600+
}

docs/docs/fundamentals/core-concepts.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ does and help you understand further chapters better.
1414
every keystroke back into React state. It owns its content on the native side
1515
and you talk to it through a `ref`.
1616

17-
This is deliberate. Rich text changes constantly every character, selection
18-
move and style toggle and round-tripping all of that through JavaScript state
17+
This is deliberate. Rich text changes constantly - every character, selection
18+
move and style toggle - and round-tripping all of that through JavaScript state
1919
would be extremely slow and open to a possible de-synchronization of those states.
2020
Keeping it native makes the editor fast and stable.
2121

@@ -56,8 +56,8 @@ All supported and canonical tags are listed in [Supported tags](/fundamentals/ht
5656

5757
The library is split into an editor and a viewer:
5858

59-
- **`EnrichedTextInput`** the interactive editor from the previous page.
60-
- **`EnrichedText`** a read-only display component that renders the input's
59+
- **`EnrichedTextInput`** - the interactive editor from the previous page.
60+
- **`EnrichedText`** - a read-only display component that renders the input's
6161
HTML.
6262

6363
The HTML format that both components expect is identical, what allows you to integrate them seamlessly. A common setup edits in `EnrichedTextInput`, stores the `getHTML` output, and later displays it with `EnrichedText`.
@@ -66,12 +66,12 @@ The HTML format that both components expect is identical, what allows you to int
6666

6767
Not every style can be combined with every other. For example, a paragraph can't be both a heading and a list item, code blocks don't support inline formatting such as bold or italic. The editor tracks this and reports it through `onChangeState`, which gives each style three booleans:
6868

69-
- **`isActive`** the style is applied at the current selection. Use it to
69+
- **`isActive`** - the style is applied at the current selection. Use it to
7070
highlight a toolbar button.
71-
- **`isBlocking`** another active style forbids this one entirely, so toggling
71+
- **`isBlocking`** - another active style forbids this one entirely, so toggling
7272
it would do nothing. For example bold is blocked inside a code block. Use it
7373
to disable a button.
74-
- **`isConflicting`** this style would replace an active one if toggled on.
74+
- **`isConflicting`** - this style would replace an active one if toggled on.
7575
For example switching a blockquote paragraph to a heading removes the
7676
blockquote. Use it to hint that the toggle is a swap, not an addition.
7777

docs/docs/fundamentals/getting-started.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ React Native Enriched HTML is a rich text solution for React Native built by
1919

2020
It ships two fully native components: `EnrichedTextInput`, a rich text editor
2121
that styles text live as you type, and `EnrichedText`, a read-only display component that
22-
renders the input's output. Both speak HTML the input
22+
renders the input's output. Both speak HTML - the input
2323
produces it, the display consumes it.
2424

2525
Not only does this library allow you to apply basic rich text styles you know well,

docs/docs/fundamentals/html-format-and-supported-tags.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,18 @@ sidebar_position: 4
44

55
# HTML format and supported tags
66

7-
The editor works with a fixed set of standard and custom HTML tags it both
7+
The editor works with a fixed set of standard and custom HTML tags - it both
88
produces them in its output and accepts them as input. This page is the
99
reference for that set.
1010

1111
Styles fall into two groups: **inline** tags that wrap a range of characters,
1212
and **paragraph** tags that apply to whole lines. Not all of them combine
1313
freely, and there are two kinds of restriction:
1414

15-
- **Conflicting** toggling a style that conflicts with an active one replaces
15+
- **Conflicting** - toggling a style that conflicts with an active one replaces
1616
it. Toggling `<h2>` on a `<blockquote>` paragraph removes the blockquote and
1717
applies the heading.
18-
- **Blocking** a blocked style can't be toggled at all while the blocking
18+
- **Blocking** - a blocked style can't be toggled at all while the blocking
1919
style is active. `<b>` is blocked inside `<codeblock>`, so bold can't be
2020
applied there.
2121

@@ -45,7 +45,7 @@ of it is redundant and therefore blocked.
4545

4646
## Paragraph tags
4747

48-
Only one paragraph-level style can be active per paragraph they all conflict
48+
Only one paragraph-level style can be active per paragraph - they all conflict
4949
with each other.
5050

5151
Some paragraph styles are containers that wrap each line inside them with an

docs/docs/fundamentals/your-first-editor.mdx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ the library: **calling methods on the input** and **reading its state back**.
1515
## Rendering the input
1616

1717
Start by dropping an `EnrichedTextInput` on the screen. It behaves like a
18-
regular `TextInput` give it a `style` and a `placeholder` and you have a
18+
regular `TextInput` - give it a `style` and a `placeholder` and you have a
1919
working editor.
2020

2121
```tsx
@@ -48,7 +48,7 @@ const styles = StyleSheet.create({
4848

4949
## Toggling a style
5050

51-
The input is uncontrolled you don't change its content through props. Instead
51+
The input is uncontrolled - you don't change its content through props. Instead
5252
you call methods on it through a `ref` of type `EnrichedTextInputInstance`.
5353

5454
Add a ref and a button that calls `toggleBold`. Each call flips bold on the
@@ -103,7 +103,7 @@ const styles = StyleSheet.create({
103103
});
104104
```
105105

106-
Every other style is toggled similarly via `ref` there's a `toggleItalic`,
106+
Every other style is toggled similarly via `ref` - there's a `toggleItalic`,
107107
`toggleUnderline`, `toggleH1`, `toggleOrderedList`, and so on.
108108

109109
## Reading the state back
@@ -194,7 +194,7 @@ lists which styles clash with each other.
194194

195195
## Try it out
196196

197-
Here's that editor running live on this page. Select some text and hit **Bold**
197+
Here's that editor running live on this page. Select some text and hit **Bold** -
198198
notice the button turns green whenever the cursor sits on bold text. Switch to
199199
the **Code** tab to see the exact source.
200200

docs/docs/rich-text-formatting/basic-styles.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ In [Your first editor](/fundamentals/your-first-editor) you wired up a single
1313
`toggle` method on the `ref` changes the text, and `onChangeState` reports back
1414
whether the style is active. The only exceptions to that rule are **mentions**,
1515
**links** and **inline images**, which work a bit differently. They cannot be toggled
16-
on/off on any text - they require additional data to be valid,but you'll learn more
16+
on/off on any text - they require additional data to be valid, but you'll learn more
1717
about them in further sections. This page walks through the rest of the basic rich
1818
text styles and the one core distinction that shapes how they behave - **inline**
1919
versus **paragraph** formatting.

docs/docs/rich-text-formatting/links.mdx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,11 +104,15 @@ while editing an autolinked token breaks it.
104104

105105
<InteractiveExample src={LinksEditorSrc} component={LinksEditor} />
106106

107+
## Handling links with events
108+
109+
There is an event that allows you to handle user's interaction with links inside `EnrichedTextInput`.
110+
111+
- **`onLinkDetected`** fires when the cursor enters or leaves a created link.
112+
107113
:::info
108114

109-
This page covers creating and detecting links. To react to links the user
110-
touches, listen for the
111-
`onLinkDetected` event, and to change how
112-
links look, see [Styling the input](/core-functionalities/styling-the-input).
115+
This page covers creating and detecting links. To change how
116+
they look, see [Styling the input](/core-functionalities/styling-the-input).
113117

114118
:::

0 commit comments

Comments
 (0)