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
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.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
codeBlockStyledDefaults,
fontFamilyControl,
fontWeightControl,
githubFlavorArgTypes,
type CodeBlockStyleControls,
numberControl,
} from '../shared/storybookMarkdownStyles';
Expand All @@ -14,7 +15,7 @@ import {
} from '../shared/storybookStyleBuilders';
import type { TextStory } from '../shared/storyTypes';

const MARKDOWN = `\`\`\`
const MARKDOWN = `\`\`\`python
sum = 0
for i in range(20):
print(i % 3)
Expand All @@ -24,6 +25,9 @@ print(sum)
\`\`\``;

const argTypes = {
...githubFlavorArgTypes(
'commonmark — code block rendered as spans inside the single TextView. github — code block rendered as a separate block component.'
),
fontSize: numberControl('markdownStyle.codeBlock.fontSize', {
min: 10,
max: 20,
Expand Down Expand Up @@ -80,6 +84,7 @@ export default storyMeta('Block', 'Code Block');
export const Default: TextStory<CodeBlockStyleControls> = {
args: {
markdown: MARKDOWN,
flavor: 'github',
...codeBlockStyledDefaults,
},
argTypes,
Expand All @@ -91,7 +96,7 @@ export const Default: TextStory<CodeBlockStyleControls> = {
return (
<EnrichedMarkdownTextStory
title="Code Block"
description="Fenced code blocks with triple backticks. Use the controls to tune markdownStyle.codeBlock."
description="Fenced code blocks with triple backticks. Use the flavor control to switch between the span-based (commonmark) and block-component (github) renderers, and the other controls to tune markdownStyle.codeBlock."
{...rest}
style={{ codeBlock: toCodeBlockStyle(controls) }}
/>
Expand Down
6 changes: 4 additions & 2 deletions docs/API_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,15 +233,17 @@ Code blocks are always rendered left-to-right regardless of this prop. Per-parag

### `flavor`

Markdown flavor. Set to `'github'` to enable GitHub Flavored Markdown table support.
Markdown flavor. Set to `'github'` to enable GitHub Flavored Markdown features: tables and block-style code blocks.

| Type | Default Value | Platform |
| --------------------------------- | --------------- | -------- |
| `'commonmark' \| 'github'` | `'commonmark'` | Both |

> **Note:**
> - **`'commonmark'`**: All Markdown content is rendered as a single TextView. Selecting text will select all content in the view.
> - **`'github'`**: The Markdown AST is split into segments. Consecutive text blocks (paragraphs, headings, lists, etc.) are grouped into separate TextView segments, while tables are rendered as separate table views. This allows for granular text selection within each segment and enables interactive table features (horizontal scrolling, context menus). Text selection cannot span across segments.
> - **`'github'`**: The Markdown AST is split into segments. Consecutive text blocks (paragraphs, headings, lists, etc.) are grouped into separate TextView segments, while tables, fenced code blocks, and math blocks are rendered as separate block views. This allows for granular text selection within each segment and enables interactive block features (horizontal table scrolling, context menus, the code block header). Text selection cannot span across segments.
>
> With `'github'`, a fenced code block renders as a dedicated component: a header bar with the language display name (` ```python ` shows "Python") and a copy-code button, a divider, and the code below. Long lines do not wrap — the code pane scrolls horizontally while the header stays fixed. Long-pressing the block opens the Copy / Copy as Markdown menu. With `'commonmark'`, code blocks stay inside the single TextView, styled via spans, and long lines wrap.

### `streamingAnimation`

Expand Down
7 changes: 5 additions & 2 deletions docs/COPY_OPTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,11 @@ Notes:
plural category (`zero`, `one`, `two`, `few`, `many`, `other`). Only `other` is
required; any category left `undefined` falls back to it. The `{count}` token
is replaced by the number of selected images.
- The labels apply to the main text selection menu as well as the table and math
block copy menus.
- The labels apply to the main text selection menu as well as the table, math,
and code block copy menus. The code block header's copy button also reuses
the copy label for assistive technologies: it is the button's
contentDescription on Android, and on iOS it names the VoiceOver custom
action that triggers the copy.
- OS-provided actions (Look Up, Translate…) and the system **Cut / Paste /
Select All** items are localized by the platform and are not affected by this
config.
Expand Down
2 changes: 1 addition & 1 deletion docs/ELEMENTS_STRUCTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Markdown elements in `react-native-enriched-markdown` are organized into block a
| Headings | `# H1` to `###### H6` | `h1` - `h6` | Six levels of headings |
| Paragraphs | Plain text | `paragraph` | Default text container |
| Blockquotes | `> Quote` | `blockquote` | Quoted content with accent bar, unlimited nesting |
| Code Blocks | ` ``` code ``` ` | `codeBlock` | Multi-line code containers |
| Code Blocks | ` ``` code ``` ` | `codeBlock` | Multi-line code containers; with `flavor="github"` rendered as a block component with a language header and copy-code button |
| Unordered Lists | `- Item`, `* Item`, or `+ Item` | `list` | Bullet lists with unlimited nesting |
| Ordered Lists | `1. Item` | `list` | Numbered lists with unlimited nesting |
| Task Lists | `- [x] Done`, `- [ ] Todo` | `taskList` | Interactive checkboxes (requires `flavor="github"`) |
Expand Down
3 changes: 3 additions & 0 deletions docs/STYLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,9 @@ function App() {
> [!NOTE]
> Inside list items, code blocks (background included) indent to the item's content column.

> [!NOTE]
> With `flavor="github"`, code blocks render as a block component with a header bar (language name on the left, copy-code button on the right) and a divider above the code. The header derives its appearance from the code block style: the label uses the system font at 0.85 x `fontSize`, and the label, button, and divider use `color` at reduced opacity. Dedicated header style properties may be added later.

### Inline Code-specific

| Property | Type | Description |
Expand Down
4 changes: 2 additions & 2 deletions packages/core/EnrichedMarkdownCore.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ Pod::Spec.new do |s|

s.platforms = { :ios => min_ios_version_supported, :osx => "14.0" }

s.source_files = "cpp/md4c/*.{c,h}", "cpp/parser/*.{hpp,cpp}"
s.source_files = "cpp/md4c/*.{c,h}", "cpp/parser/*.{hpp,cpp}", "cpp/highlight/*.{hpp,cpp}"
s.private_header_files = "cpp/**/*.{h,hpp}"

s.pod_target_xcconfig = {
"HEADER_SEARCH_PATHS" => '"$(PODS_TARGET_SRCROOT)/cpp/md4c" "$(PODS_TARGET_SRCROOT)/cpp/parser"',
"HEADER_SEARCH_PATHS" => '"$(PODS_TARGET_SRCROOT)/cpp/md4c" "$(PODS_TARGET_SRCROOT)/cpp/parser" "$(PODS_TARGET_SRCROOT)/cpp/highlight"',
"GCC_PREPROCESSOR_DEFINITIONS" => "$(inherited) MD4C_USE_UTF8=1",
"CLANG_CXX_LANGUAGE_STANDARD" => "c++17"
}
Expand Down
17 changes: 17 additions & 0 deletions packages/core/cpp/highlight/CodeBlockHighlighter.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#include "CodeBlockHighlighter.hpp"

// Stub compiled when the syntax highlighting module is disabled. The real
// implementation defines the same symbol under ENRICHED_MARKDOWN_CODE_HIGHLIGHT,
// so exactly one definition exists in any build configuration.

#if !defined(ENRICHED_MARKDOWN_CODE_HIGHLIGHT)

namespace Markdown {

std::vector<HighlightToken> highlightCode(const std::string & /*code*/, const std::string & /*language*/) {
return {};
}

} // namespace Markdown

#endif
72 changes: 72 additions & 0 deletions packages/core/cpp/highlight/CodeBlockHighlighter.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#pragma once

#include <cstdint>
#include <string>
#include <vector>

// Shared seam for optional syntax highlighting of fenced code blocks.
//
// The real implementation is compiled only when ENRICHED_MARKDOWN_CODE_HIGHLIGHT
// is defined by the build. Without the flag the stub in CodeBlockHighlighter.cpp
// returns no tokens and platform callers keep the plain uncolored rendering, so
// the absent module degrades to the default code block appearance. The same
// fallback applies at runtime when a language has no compiled grammar or
// highlighting fails.
//
// The seam returns semantic tokens instead of a platform text type on
// purpose. Platform adapters map token types to foreground colors and apply
// them to their attributed string. Since foreground color never affects text
// metrics, the block height measured from the plain string is guaranteed to
// match the drawn height regardless of what an implementation returns.
//
// Token offsets are UTF-16 code units into the code string, because both
// Android (Spannable) and iOS (NSAttributedString) address text in UTF-16.
// Implementations are responsible for converting tree-sitter byte offsets.
//
// Token types follow tree-sitter's standard highlight capture names,
// flattened to one level. Values are explicit because they cross the JNI
// boundary as plain integers.

// TODO: phase 2 adds the real implementation next to this file: the
// tree-sitter runtime plus a vendored set of language grammars and their
// highlight queries, compiled only when ENRICHED_MARKDOWN_CODE_HIGHLIGHT is
// defined by the gradle property / podspec option.
//
// TODO: only the github flavor calls this seam; the commonmark flavor renders
// code blocks uncolored. To hook it up, the commonmark code block renderers
// (CodeBlockRenderer.kt / CodeBlockRenderer.m) would call highlightCode with
// the shared node helpers (CodeBlockNode.kt / ENRMCodeBlockContent.h) and
// apply the tokens to their content range through the platform adapters.

namespace Markdown {

enum class HighlightTokenType : uint8_t {
Keyword = 0,
Operator = 1,
Punctuation = 2,
String = 3,
Number = 4,
Constant = 5,
Comment = 6,
Function = 7,
Type = 8,
Variable = 9,
Property = 10,
Tag = 11,
Attribute = 12,
Embedded = 13,
};

struct HighlightToken {
uint32_t start;
uint32_t end;
HighlightTokenType type;
};

// code is the UTF-8 code block content without the fence lines; language is
// the fence info string (for example "python"), empty when absent. Returns an
// empty vector whenever highlighting is unavailable (module compiled out,
// unknown language, parse failure).
std::vector<HighlightToken> highlightCode(const std::string& code, const std::string& language);

} // namespace Markdown
88 changes: 88 additions & 0 deletions packages/core/cpp/highlight/CodeBlockLanguages.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
#include "CodeBlockLanguages.hpp"

#include <algorithm>
#include <array>
#include <cctype>
#include <cstring>

namespace Markdown {

namespace {

struct LanguageName {
const char *key;
const char *name;
};

// Sorted by key; displayNameForLanguage binary-searches this table.
constexpr std::array<LanguageName, 44> kLanguageNames{{
{"bash", "Bash"},
{"c", "C"},
{"cc", "C++"},
{"cpp", "C++"},
{"cs", "C#"},
{"csharp", "C#"},
{"css", "CSS"},
{"cxx", "C++"},
{"dockerfile", "Dockerfile"},
{"go", "Go"},
{"golang", "Go"},
{"graphql", "GraphQL"},
{"html", "HTML"},
{"java", "Java"},
{"javascript", "JavaScript"},
{"js", "JavaScript"},
{"json", "JSON"},
{"jsx", "JSX"},
{"kotlin", "Kotlin"},
{"kt", "Kotlin"},
{"markdown", "Markdown"},
{"md", "Markdown"},
{"objc", "Objective-C"},
{"objectivec", "Objective-C"},
{"php", "PHP"},
{"py", "Python"},
{"python", "Python"},
{"rb", "Ruby"},
{"ruby", "Ruby"},
{"rs", "Rust"},
{"rust", "Rust"},
{"scss", "SCSS"},
{"sh", "Shell"},
{"shell", "Shell"},
{"sql", "SQL"},
{"swift", "Swift"},
{"toml", "TOML"},
{"ts", "TypeScript"},
{"tsx", "TSX"},
{"typescript", "TypeScript"},
{"xml", "XML"},
{"yaml", "YAML"},
{"yml", "YAML"},
{"zsh", "Zsh"},
}};

} // namespace

std::string displayNameForLanguage(const std::string &language) {
if (language.empty()) {
return "";
}

std::string lower = language;
for (char &c : lower) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}

auto it =
std::lower_bound(kLanguageNames.begin(), kLanguageNames.end(), lower.c_str(),
[](const LanguageName &entry, const char *key) { return std::strcmp(entry.key, key) < 0; });
if (it != kLanguageNames.end() && lower == it->key) {
return it->name;
}

lower[0] = static_cast<char>(std::toupper(static_cast<unsigned char>(lower[0])));
return lower;
}

} // namespace Markdown
18 changes: 18 additions & 0 deletions packages/core/cpp/highlight/CodeBlockLanguages.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#pragma once

#include <string>

// Display names for code block fence languages, shared by every platform and
// both markdown flavors so a fence like ```python is labeled identically
// everywhere. Unlike the highlighting seam this is always compiled; it does
// not depend on ENRICHED_MARKDOWN_CODE_HIGHLIGHT.

namespace Markdown {

// Maps a fence info string (for example "python", "js") to a human readable
// display name ("Python", "JavaScript"). Unknown languages fall back to the
// lowercased input with the first ASCII letter capitalized. Empty input
// returns an empty string.
std::string displayNameForLanguage(const std::string& language);

} // namespace Markdown
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Pod::Spec.new do |s|
s.dependency "EnrichedMarkdownCore"
else
s.private_header_files = "ios/**/*.h", "cpp/**/*.{h,hpp}"
s.source_files = "ios/**/*.{h,m,mm,cpp,swift}", "cpp/md4c/*.{c,h}", "cpp/parser/*.{hpp,cpp}"
s.source_files = "ios/**/*.{h,m,mm,cpp,swift}", "cpp/md4c/*.{c,h}", "cpp/parser/*.{hpp,cpp}", "cpp/highlight/*.{hpp,cpp}"
end

# To disable LaTeX math rendering (RaTeX, iOS only), add ENV['ENRICHED_MARKDOWN_ENABLE_MATH'] = '0' to your Podfile.
Expand Down Expand Up @@ -55,7 +55,7 @@ Pod::Spec.new do |s|
end

pod_xcconfig = {
'HEADER_SEARCH_PATHS' => "\"#{cpp_root}/md4c\" \"#{cpp_root}/parser\" \"$(PODS_TARGET_SRCROOT)/ios/internals\" \"$(PODS_TARGET_SRCROOT)/ios/input/internals\"",
'HEADER_SEARCH_PATHS' => "\"#{cpp_root}/md4c\" \"#{cpp_root}/parser\" \"#{cpp_root}/highlight\" \"$(PODS_TARGET_SRCROOT)/ios/internals\" \"$(PODS_TARGET_SRCROOT)/ios/input/internals\"",
'GCC_PREPROCESSOR_DEFINITIONS' => preprocessor_defs,
'CLANG_CXX_LANGUAGE_STANDARD' => 'c++17',
'DEFINES_MODULE' => 'YES'
Expand Down
Loading
Loading