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,33 @@
appId: swmansion.enriched.markdown.example
tags:
- block
- advanced
- list
- code_block
---
# checks list items whose first (and only) child is a fenced code block —
# the marker must still be drawn in the gutter next to the code block.
- launchApp

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

- runFlow:
file: '../../../subflows/set_enriched_text_value.yaml'
env:
VALUE: |
1. ```
npm install
```
2. ```
gh auth login
```

- ```
echo bullet
```

- runFlow:
file: '../../../subflows/capture_or_assert_screenshot.yaml'
env:
SCREENSHOT_NAME: 'list_code_block_first_combo_display'
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
appId: swmansion.enriched.markdown.example
tags:
- block
- advanced
- list
- code_block
- text
---
# checks a "loose" ordered list where items and their fenced code blocks
# are separated by blank lines (each item wraps its content in Paragraph nodes).
- launchApp

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

- runFlow:
file: '../../../subflows/set_enriched_text_value.yaml'
env:
VALUE: |
1. Install via npm:
```
npm install
```

2. Authenticate with GitHub:
```
gh auth login
```

This opens your browser for OAuth. Once authorized, tokens are stored locally at `~/.config/gh/tokens.json`.

3. Verify the connection:
```
gh repo list
```

- runFlow:
file: '../../../subflows/capture_or_assert_screenshot.yaml'
env:
SCREENSHOT_NAME: 'list_code_block_loose_combo_display'
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
appId: swmansion.enriched.markdown.example
tags:
- block
- advanced
- list
- code_block
- text
---
# checks a "tight" ordered list where items and their fenced code blocks
# are directly adjacent (no blank lines between).
- launchApp

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

- runFlow:
file: '../../../subflows/set_enriched_text_value.yaml'
env:
VALUE: |
1. Install via npm:
```
npm install
```
2. Authenticate with GitHub:
```
gh auth login
```
This opens your browser for OAuth. Once authorized, tokens are stored locally at `~/.config/gh/tokens.json`.
3. Verify the connection:
```
gh repo list
```

- runFlow:
file: '../../../subflows/capture_or_assert_screenshot.yaml'
env:
SCREENSHOT_NAME: 'list_code_block_tight_combo_display'
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
appId: swmansion.enriched.markdown.example
tags:
- block
- advanced
- list
- text
---
# checks a list item whose first child is a nested sublist — both the outer
# and inner markers must be drawn on the shared first line, at their own gutters.
- launchApp

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

- runFlow:
file: '../../../subflows/set_enriched_text_value.yaml'
env:
VALUE: |
1.
- alpha
- beta
2. gamma

- runFlow:
file: '../../../subflows/capture_or_assert_screenshot.yaml'
env:
SCREENSHOT_NAME: 'list_sublist_first_combo_display'
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
appId: swmansion.enriched.markdown.example
tags:
- block
- advanced
- list
- task_list
- code_block
---
# checks a checked task item containing a fenced code block — the checked
# text decorations (strikethrough / color) must not bleed into the code block.
- launchApp

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

- runFlow:
file: '../../../subflows/set_enriched_text_value.yaml'
env:
VALUE: |
- [x] setup complete
```
npm i
```
- [ ] still pending

- runFlow:
file: '../../../subflows/capture_or_assert_screenshot.yaml'
env:
SCREENSHOT_NAME: 'task_list_code_block_combo_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.
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.
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.
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.
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.
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.
Comment thread
eszlamczyk marked this conversation as resolved.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 16 additions & 0 deletions docs/ELEMENTS_STRUCTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,22 @@ Markdown elements in `react-native-enriched-markdown` are organized into block a
2. Second item
```

### Lists with Block Content Example

List items can contain block elements — fenced code blocks, nested lists, and multiple paragraphs:

````markdown
1. Install via npm:

```
npm install
```

2. Verify the connection.
````

Code blocks indent to the item's content column, and the marker is drawn next to the first line even when a code block or nested list is the item's first child.

### Nested Blockquotes Example

```markdown
Expand Down
3 changes: 3 additions & 0 deletions docs/STYLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ function App() {
| `borderWidth` | `number` | Border width |
| `padding` | `number` | Inner padding |

> [!NOTE]
> Inside list items, code blocks (background included) indent to the item's content column.

### Inline Code-specific

| Property | Type | Description |
Expand Down
86 changes: 86 additions & 0 deletions packages/core/cpp/parser/MD4CParser.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,91 @@ void promoteDisplayMathFromParagraphs(MarkdownASTNode &root) {
}
}

bool isBlockNode(const MarkdownASTNode &node) {
switch (node.type) {
case NodeType::Document:
case NodeType::Paragraph:
case NodeType::Heading:
case NodeType::Blockquote:
case NodeType::UnorderedList:
case NodeType::OrderedList:
case NodeType::ListItem:
case NodeType::CodeBlock:
case NodeType::ThematicBreak:
case NodeType::LatexMathDisplay:
case NodeType::Table:
case NodeType::TableHead:
case NodeType::TableBody:
case NodeType::TableRow:
case NodeType::TableHeaderCell:
case NodeType::TableCell:
return true;
default:
return false;
}
}
Comment thread
eszlamczyk marked this conversation as resolved.

// md4c omits Paragraph wrappers around the inline content of tight list items.
// Wrap each run of consecutive inline children of a ListItem in a synthetic
// Paragraph (marked "tight") so renderers only ever see block children.
void wrapListItemInlineRuns(MarkdownASTNode &node) {
for (auto &child : node.children) {
wrapListItemInlineRuns(*child);
}

if (node.type != NodeType::ListItem)
return;

bool hasInlineChild = false;
for (auto &child : node.children) {
if (!isBlockNode(*child)) {
hasInlineChild = true;
break;
}
}
if (!hasInlineChild)
return;

std::vector<std::shared_ptr<MarkdownASTNode>> newChildren;
newChildren.reserve(node.children.size());
std::vector<std::shared_ptr<MarkdownASTNode>> run;

auto flushRun = [&]() {
if (run.empty())
return;
bool hasContent = false;
for (auto &n : run) {
if (!isSeparatorNode(*n)) {
hasContent = true;
break;
}
}
if (hasContent) {
auto paragraph = std::make_shared<MarkdownASTNode>(NodeType::Paragraph);
paragraph->setAttribute("tight", "true");
paragraph->children = std::move(run);
newChildren.push_back(std::move(paragraph));
} else {
// Whitespace-only run between blocks — keep as-is, a paragraph would add spacing.
for (auto &n : run) {
newChildren.push_back(std::move(n));
}
}
run.clear();
};

for (auto &child : node.children) {
if (isBlockNode(*child)) {
flushRun();
newChildren.push_back(std::move(child));
} else {
run.push_back(std::move(child));
}
}
flushRun();
node.children = std::move(newChildren);
}

} // anonymous namespace

MD4CParser::MD4CParser() : impl_(std::make_unique<Impl>()) {}
Expand Down Expand Up @@ -522,6 +607,7 @@ std::shared_ptr<MarkdownASTNode> MD4CParser::parse(const std::string &markdown,

if (impl_->root) {
promoteDisplayMathFromParagraphs(*impl_->root);
wrapListItemInlineRuns(*impl_->root);
}

return impl_->root ? impl_->root : std::make_shared<MarkdownASTNode>(NodeType::Document);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class BlockStyleContext {
var listType: ListType? = null
var listItemNumber = 0
var taskItemCount = 0
var accumulatedIndent = 0

private val orderedListItemNumbers = ArrayDeque<Int>()

Expand Down Expand Up @@ -147,6 +148,7 @@ class BlockStyleContext {
listType = null
listItemNumber = 0
taskItemCount = 0
accumulatedIndent = 0
orderedListItemNumbers.clear()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ class BlockquoteRenderer(
onLinkLongPress: ((String) -> Unit)?,
factory: RendererFactory,
) {
if (builder.isNotEmpty() && builder.last() != '\n') {
builder.append("\n")
}

val start = builder.length
val style = config.style.blockquoteStyle
val context = factory.blockStyleContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ class CodeBlockRenderer(
onLinkLongPress: ((String) -> Unit)?,
factory: RendererFactory,
) {
if (builder.isNotEmpty() && builder.last() != '\n') {
builder.append("\n")
}

val start = builder.length
val style = config.style.codeBlockStyle
val context = factory.blockStyleContext
Expand All @@ -44,7 +48,7 @@ class CodeBlockRenderer(

// Apply background, borders, and horizontal padding to content only
builder.setSpan(
CodeBlockSpan(style, factory.context, factory.styleCache),
CodeBlockSpan(style, factory.context, factory.styleCache, context.accumulatedIndent),
contentStart,
end,
SPAN_FLAGS_EXCLUSIVE_EXCLUSIVE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ class HeadingRenderer(
onLinkLongPress: ((String) -> Unit)?,
factory: RendererFactory,
) {
if (builder.isNotEmpty() && builder.last() != '\n') {
builder.append("\n")
}

val level = node.getAttribute("level")?.toIntOrNull() ?: 1
val start = builder.length

Expand Down
Loading
Loading