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
@@ -0,0 +1,29 @@
appId: swmansion.enriched.markdown.example
tags:
- smoke
- block
- paragraph
- text
---
# A trailing backslash is a CommonMark hard break: it forces a line break while
# staying inside one paragraph, so the two lines are separated by line height
# only. The blank line then starts a second paragraph that carries paragraph
# margins — the screenshot pins the spacing difference between the two.
- launchApp

- runFlow:
file: '../../../../subflows/move_to_playground.yaml'

- runFlow:
file: '../../../subflows/set_enriched_text_value.yaml'
env:
VALUE: |
This line ends with a backslash\
so this text continues inside the same paragraph.

A blank line starts a new paragraph with its own spacing.

- runFlow:
file: '../../../subflows/capture_or_assert_screenshot.yaml'
env:
SCREENSHOT_NAME: 'hard_break_display'
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
appId: swmansion.enriched.markdown.example
tags:
- smoke
- block
- paragraph
- text
---
# Regression for #541: a single newline inside a paragraph is a CommonMark soft
# break and renders as a space — both lines flow together into one wrapped
# paragraph with no forced line break after "your".
- launchApp

- runFlow:
file: '../../../../subflows/move_to_playground.yaml'

- runFlow:
file: '../../../subflows/set_enriched_text_value.yaml'
env:
VALUE: |
Let's start simple: keep taking your medications as prescribed, and we'll begin tracking your
blood sugar and blood pressure so we can see how you're really doing.

- runFlow:
file: '../../../subflows/capture_or_assert_screenshot.yaml'
env:
SCREENSHOT_NAME: 'soft_break_display'
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 20 additions & 1 deletion docs/ELEMENTS_STRUCTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,33 @@ Block elements are structural containers that define the layout and establish th

Inline elements modify text within blocks and apply additional styling on top of the block's typography.

## Line Breaks

Newlines follow standard CommonMark semantics:

- **Blank line**: Starts a new paragraph.
- **Single newline (soft break)**: Renders as a space — consecutive lines flow together into one wrapped paragraph, matching how GitHub and other CommonMark renderers display Markdown.
- **Hard break**: Ends a line with two spaces or a backslash to force a line break within the paragraph.

```markdown
This line and
this line render as one continuous sentence.

A blank line starts a new paragraph.

Two trailing spaces
or a trailing backslash\
force a line break within the paragraph.
```

## Images: Block vs Inline

Images are automatically detected as block or inline based on context:

- **Block images**: When an image is the only content in a paragraph (standalone), it's treated as a block image and uses block-level spacing
- **Inline images**: When an image appears alongside other text content, it's treated as inline and aligns with the text baseline

You don't need to specify which type—the renderer automatically determines this based on the image's position in the content.
You don't need to specify which type—the renderer automatically determines this based on the image's position in the content. Note that a single newline doesn't split a paragraph, so an image on its own source line directly below text is still inline; separate it with a blank line to make it a block image.

## Nested Elements

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ namespace {

static_assert(static_cast<int>(NodeType::Highlight) == 29,
"NodeType enum must stay in sync with Kotlin MarkdownASTNode.NodeType");
static_assert(static_cast<int>(NodeType::SoftBreak) == 30,
"NodeType enum must stay in sync with Kotlin MarkdownASTNode.NodeType");

Comment thread
eszlamczyk marked this conversation as resolved.
local_ref<JMarkdownASTNode> createJavaNode(const std::shared_ptr<MarkdownASTNode> &node) {
if (!node) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ data class MarkdownASTNode(
Superscript,
Subscript,
Highlight,
SoftBreak,
}

fun getAttribute(key: String): String? = attributes[key]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class RendererFactory(

private val textRenderer = TextRenderer()
private val lineBreakRenderer = LineBreakRenderer()
private val softBreakRenderer = SoftBreakRenderer()

private val renderers: Map<MarkdownASTNode.NodeType, NodeRenderer> by lazy {
buildMap {
Expand All @@ -92,6 +93,7 @@ class RendererFactory(
put(MarkdownASTNode.NodeType.Code, CodeRenderer(config))
put(MarkdownASTNode.NodeType.Image, ImageRenderer())
put(MarkdownASTNode.NodeType.LineBreak, lineBreakRenderer)
put(MarkdownASTNode.NodeType.SoftBreak, softBreakRenderer)
put(MarkdownASTNode.NodeType.ThematicBreak, ThematicBreakRenderer(config))
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.swmansion.enriched.markdown.renderer

import android.text.SpannableStringBuilder
import com.swmansion.enriched.markdown.parser.MarkdownASTNode

class SoftBreakRenderer : NodeRenderer {
override fun render(
node: MarkdownASTNode,
builder: SpannableStringBuilder,
onLinkPress: ((String) -> Unit)?,
onLinkLongPress: ((String) -> Unit)?,
factory: RendererFactory,
) {
builder.append(" ")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ import com.swmansion.enriched.markdown.test.TestAstFactory.document
import com.swmansion.enriched.markdown.test.TestAstFactory.emphasis
import com.swmansion.enriched.markdown.test.TestAstFactory.heading
import com.swmansion.enriched.markdown.test.TestAstFactory.image
import com.swmansion.enriched.markdown.test.TestAstFactory.lineBreak
import com.swmansion.enriched.markdown.test.TestAstFactory.link
import com.swmansion.enriched.markdown.test.TestAstFactory.listItem
import com.swmansion.enriched.markdown.test.TestAstFactory.orderedList
import com.swmansion.enriched.markdown.test.TestAstFactory.paragraph
import com.swmansion.enriched.markdown.test.TestAstFactory.softBreak
import com.swmansion.enriched.markdown.test.TestAstFactory.strong
import com.swmansion.enriched.markdown.test.TestAstFactory.text
import com.swmansion.enriched.markdown.test.TestAstFactory.thematicBreak
Expand All @@ -47,6 +49,38 @@ class CommonMarkRendererTest {
rendered.assertContains("Hello CommonMark")
}

@Test
fun rendersSoftBreakAsSpace() {
val rendered =
render(
document(
paragraph(
text("tracking your"),
softBreak(),
text("blood sugar"),
),
),
)

rendered.assertContains("tracking your blood sugar")
}

@Test
fun rendersHardBreakAsNewline() {
val rendered =
render(
document(
paragraph(
text("line one"),
lineBreak(),
text("line two"),
),
),
)

rendered.assertContains("line one\nline two")
}

@Test
fun rendersBoldText() {
val rendered =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,8 @@ object TestAstFactory {
)

fun thematicBreak(): MarkdownASTNode = MarkdownASTNode(NodeType.ThematicBreak)

fun softBreak(): MarkdownASTNode = MarkdownASTNode(NodeType.SoftBreak)

fun lineBreak(): MarkdownASTNode = MarkdownASTNode(NodeType.LineBreak)
}
13 changes: 8 additions & 5 deletions packages/core/cpp/parser/MD4CParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -354,10 +354,13 @@ class MD4CParser::Impl {
return 0;
auto *impl = static_cast<Impl *>(userdata);

// Handle soft/hard line breaks
if (type == MD_TEXT_SOFTBR || type == MD_TEXT_BR) {
auto brNode = std::make_shared<MarkdownASTNode>(NodeType::LineBreak);
impl->addInlineNode(brNode);
if (type == MD_TEXT_SOFTBR) {
impl->addInlineNode(std::make_shared<MarkdownASTNode>(NodeType::SoftBreak));
return 0;
}

if (type == MD_TEXT_BR) {
impl->addInlineNode(std::make_shared<MarkdownASTNode>(NodeType::LineBreak));
return 0;
}

Expand All @@ -379,7 +382,7 @@ bool isDisplayMathNode(const MarkdownASTNode &node) {
}

bool isSeparatorNode(const MarkdownASTNode &node) {
return node.type == NodeType::LineBreak ||
return node.type == NodeType::LineBreak || node.type == NodeType::SoftBreak ||
(node.type == NodeType::Text && node.content.find_first_not_of(" \t\n\r") == std::string::npos);
}

Expand Down
3 changes: 2 additions & 1 deletion packages/core/cpp/parser/MarkdownASTNode.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ enum class NodeType {
Spoiler,
Superscript,
Subscript,
Highlight
Highlight,
SoftBreak
};

struct MarkdownASTNode {
Expand Down
2 changes: 2 additions & 0 deletions packages/core/cpp/wasm/ASTSerializer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ static const char *nodeTypeToString(NodeType type) {
return "Heading";
case NodeType::LineBreak:
return "LineBreak";
case NodeType::SoftBreak:
return "SoftBreak";
case NodeType::Strong:
return "Strong";
case NodeType::Emphasis:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ static jint nodeTypeToJavaOrdinal(NodeType type) {
return 28;
case NodeType::Highlight:
return 29;
case NodeType::SoftBreak:
return 30;
default:
return 0;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ object InputParser {

if (node.type == NodeType.Text) {
plainText.append(node.content)
} else if (node.type == NodeType.LineBreak) {
} else if (node.type == NodeType.LineBreak || node.type == NodeType.SoftBreak) {
plainText.append("\n")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ data class MarkdownASTNode(
Superscript,
Subscript,
Highlight,
SoftBreak,
}

fun getAttribute(key: String): String? = attributes[key]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class RendererFactory(

private val textRenderer = TextRenderer()
private val lineBreakRenderer = LineBreakRenderer()
private val softBreakRenderer = SoftBreakRenderer()

private val renderers: Map<MarkdownASTNode.NodeType, NodeRenderer> by lazy {
buildMap {
Expand All @@ -103,6 +104,7 @@ class RendererFactory(
put(MarkdownASTNode.NodeType.Code, CodeRenderer(config))
put(MarkdownASTNode.NodeType.Image, ImageRenderer())
put(MarkdownASTNode.NodeType.LineBreak, lineBreakRenderer)
put(MarkdownASTNode.NodeType.SoftBreak, softBreakRenderer)
put(MarkdownASTNode.NodeType.ThematicBreak, ThematicBreakRenderer(config))
put(MarkdownASTNode.NodeType.Spoiler, SpoilerRenderer())
if (FeatureFlags.IS_MATH_ENABLED) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.swmansion.enriched.markdown.renderer

import android.text.SpannableStringBuilder
import com.swmansion.enriched.markdown.parser.MarkdownASTNode

class SoftBreakRenderer : NodeRenderer {
override fun render(
node: MarkdownASTNode,
builder: SpannableStringBuilder,
onLinkPress: ((String) -> Unit)?,
onLinkLongPress: ((String) -> Unit)?,
factory: RendererFactory,
) {
builder.append(" ")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ object MarkdownASTSerializer {
}

NodeType.LineBreak -> {
buffer.append("\\\n")
}

NodeType.SoftBreak -> {
buffer.append("\n")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ class MathInlineRenderer(
}

private fun extractLatex(node: MarkdownASTNode): String {
if (!node.content.isNullOrEmpty()) return node.content!!
return node.children.mapNotNull { it.content }.joinToString("")
if (node.content.isNotEmpty()) return node.content
return node.children.joinToString("") { child ->
when (child.type) {
MarkdownASTNode.NodeType.SoftBreak, MarkdownASTNode.NodeType.LineBreak -> " "
else -> child.content
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ typedef NS_ENUM(NSInteger, MarkdownNodeType) {
MarkdownNodeTypeSpoiler,
MarkdownNodeTypeSuperscript,
MarkdownNodeTypeSubscript,
MarkdownNodeTypeHighlight
MarkdownNodeTypeHighlight,
MarkdownNodeTypeSoftBreak
};

@interface MarkdownASTNode : NSObject
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@
case Markdown::NodeType::Highlight:
objcType = MarkdownNodeTypeHighlight;
break;
case Markdown::NodeType::SoftBreak:
objcType = MarkdownNodeTypeSoftBreak;
break;
}

MarkdownASTNode *objcNode = [[MarkdownASTNode alloc] initWithType:objcType];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ - (void)_appendChildTextFromNode:(MarkdownASTNode *)node toBuffer:(NSMutableStri
{
if (node.content.length > 0) {
[buffer appendString:node.content];
} else if (node.type == MarkdownNodeTypeSoftBreak || node.type == MarkdownNodeTypeLineBreak) {
[buffer appendString:@" "];
}

for (MarkdownASTNode *child in node.children) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ - (NSString *)extractLatexFromNode:(MarkdownASTNode *)node
for (MarkdownASTNode *child in node.children) {
if (child.content.length > 0) {
[buffer appendString:child.content];
} else if (child.type == MarkdownNodeTypeSoftBreak || child.type == MarkdownNodeTypeLineBreak) {
[buffer appendString:@" "];
}
}
return buffer;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ - (void)renderChildrenOfNode:(MarkdownASTNode *)node
[output appendAttributedString:lineBreak];
continue;
}
if (child.type == MarkdownNodeTypeSoftBreak) {
NSAttributedString *softBreak = [[NSAttributedString alloc] initWithString:@" "
attributes:[context getTextAttributes]];
[output appendAttributedString:softBreak];
continue;
}
id<NodeRenderer> renderer = [self rendererForNodeType:child.type];
if (renderer) {
[renderer renderNode:child into:output context:context];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ static void serializeNode(MarkdownASTNode *node, NSMutableString *buffer)
break;

case MarkdownNodeTypeLineBreak:
[buffer appendString:@"\\\n"];
break;

case MarkdownNodeTypeSoftBreak:
[buffer appendString:@"\n"];
break;

Expand Down
Loading
Loading