react-native-nitro-markdown 0.4.2 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/README.md +605 -318
  2. package/android/src/main/java/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSession.kt +27 -8
  3. package/cpp/bindings/HybridMarkdownParser.cpp +216 -66
  4. package/cpp/bindings/HybridMarkdownParser.hpp +2 -0
  5. package/ios/HybridMarkdownSession.swift +33 -7
  6. package/lib/commonjs/MarkdownContext.js +2 -1
  7. package/lib/commonjs/MarkdownContext.js.map +1 -1
  8. package/lib/commonjs/headless.js +41 -5
  9. package/lib/commonjs/headless.js.map +1 -1
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/markdown-stream.js +109 -13
  12. package/lib/commonjs/markdown-stream.js.map +1 -1
  13. package/lib/commonjs/markdown.js +215 -44
  14. package/lib/commonjs/markdown.js.map +1 -1
  15. package/lib/commonjs/renderers/code.js +4 -3
  16. package/lib/commonjs/renderers/code.js.map +1 -1
  17. package/lib/commonjs/renderers/heading.js +1 -1
  18. package/lib/commonjs/renderers/heading.js.map +1 -1
  19. package/lib/commonjs/renderers/image.js +7 -5
  20. package/lib/commonjs/renderers/image.js.map +1 -1
  21. package/lib/commonjs/renderers/link.js +15 -3
  22. package/lib/commonjs/renderers/link.js.map +1 -1
  23. package/lib/commonjs/renderers/list.js +2 -2
  24. package/lib/commonjs/renderers/list.js.map +1 -1
  25. package/lib/commonjs/renderers/table.js +126 -21
  26. package/lib/commonjs/renderers/table.js.map +1 -1
  27. package/lib/commonjs/use-markdown-stream.js +16 -14
  28. package/lib/commonjs/use-markdown-stream.js.map +1 -1
  29. package/lib/commonjs/utils/incremental-ast.js +153 -0
  30. package/lib/commonjs/utils/incremental-ast.js.map +1 -0
  31. package/lib/commonjs/utils/link-security.js +21 -0
  32. package/lib/commonjs/utils/link-security.js.map +1 -0
  33. package/lib/commonjs/utils/stream-timeline.js +62 -0
  34. package/lib/commonjs/utils/stream-timeline.js.map +1 -0
  35. package/lib/module/MarkdownContext.js +2 -1
  36. package/lib/module/MarkdownContext.js.map +1 -1
  37. package/lib/module/headless.js +37 -4
  38. package/lib/module/headless.js.map +1 -1
  39. package/lib/module/index.js.map +1 -1
  40. package/lib/module/markdown-stream.js +110 -14
  41. package/lib/module/markdown-stream.js.map +1 -1
  42. package/lib/module/markdown.js +217 -46
  43. package/lib/module/markdown.js.map +1 -1
  44. package/lib/module/renderers/blockquote.js.map +1 -1
  45. package/lib/module/renderers/code.js +4 -3
  46. package/lib/module/renderers/code.js.map +1 -1
  47. package/lib/module/renderers/heading.js +1 -1
  48. package/lib/module/renderers/heading.js.map +1 -1
  49. package/lib/module/renderers/image.js +7 -5
  50. package/lib/module/renderers/image.js.map +1 -1
  51. package/lib/module/renderers/link.js +15 -3
  52. package/lib/module/renderers/link.js.map +1 -1
  53. package/lib/module/renderers/list.js +2 -2
  54. package/lib/module/renderers/list.js.map +1 -1
  55. package/lib/module/renderers/paragraph.js.map +1 -1
  56. package/lib/module/renderers/table.js +127 -22
  57. package/lib/module/renderers/table.js.map +1 -1
  58. package/lib/module/use-markdown-stream.js +16 -14
  59. package/lib/module/use-markdown-stream.js.map +1 -1
  60. package/lib/module/utils/incremental-ast.js +147 -0
  61. package/lib/module/utils/incremental-ast.js.map +1 -0
  62. package/lib/module/utils/link-security.js +15 -0
  63. package/lib/module/utils/link-security.js.map +1 -0
  64. package/lib/module/utils/stream-timeline.js +56 -0
  65. package/lib/module/utils/stream-timeline.js.map +1 -0
  66. package/lib/typescript/commonjs/Markdown.nitro.d.ts +5 -3
  67. package/lib/typescript/commonjs/Markdown.nitro.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/MarkdownContext.d.ts +26 -25
  69. package/lib/typescript/commonjs/MarkdownContext.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/headless.d.ts +15 -2
  71. package/lib/typescript/commonjs/headless.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/index.d.ts +3 -1
  73. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/markdown-stream.d.ts +7 -2
  75. package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/markdown.d.ts +62 -5
  77. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/renderers/blockquote.d.ts +3 -3
  79. package/lib/typescript/commonjs/renderers/blockquote.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/renderers/code.d.ts +5 -5
  81. package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/renderers/heading.d.ts +3 -3
  83. package/lib/typescript/commonjs/renderers/heading.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts +2 -2
  85. package/lib/typescript/commonjs/renderers/horizontal-rule.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/renderers/image.d.ts +2 -2
  87. package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/renderers/link.d.ts +3 -3
  89. package/lib/typescript/commonjs/renderers/link.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/renderers/list.d.ts +7 -7
  91. package/lib/typescript/commonjs/renderers/list.d.ts.map +1 -1
  92. package/lib/typescript/commonjs/renderers/math.d.ts +4 -4
  93. package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
  94. package/lib/typescript/commonjs/renderers/paragraph.d.ts +3 -3
  95. package/lib/typescript/commonjs/renderers/paragraph.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/renderers/table.d.ts +3 -3
  97. package/lib/typescript/commonjs/renderers/table.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts +5 -2
  99. package/lib/typescript/commonjs/specs/MarkdownSession.nitro.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/theme.d.ts +2 -2
  101. package/lib/typescript/commonjs/theme.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/use-markdown-stream.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/utils/incremental-ast.d.ts +12 -0
  104. package/lib/typescript/commonjs/utils/incremental-ast.d.ts.map +1 -0
  105. package/lib/typescript/commonjs/utils/link-security.d.ts +3 -0
  106. package/lib/typescript/commonjs/utils/link-security.d.ts.map +1 -0
  107. package/lib/typescript/commonjs/utils/stream-timeline.d.ts +11 -0
  108. package/lib/typescript/commonjs/utils/stream-timeline.d.ts.map +1 -0
  109. package/lib/typescript/module/Markdown.nitro.d.ts +5 -3
  110. package/lib/typescript/module/Markdown.nitro.d.ts.map +1 -1
  111. package/lib/typescript/module/MarkdownContext.d.ts +26 -25
  112. package/lib/typescript/module/MarkdownContext.d.ts.map +1 -1
  113. package/lib/typescript/module/headless.d.ts +15 -2
  114. package/lib/typescript/module/headless.d.ts.map +1 -1
  115. package/lib/typescript/module/index.d.ts +3 -1
  116. package/lib/typescript/module/index.d.ts.map +1 -1
  117. package/lib/typescript/module/markdown-stream.d.ts +7 -2
  118. package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
  119. package/lib/typescript/module/markdown.d.ts +62 -5
  120. package/lib/typescript/module/markdown.d.ts.map +1 -1
  121. package/lib/typescript/module/renderers/blockquote.d.ts +3 -3
  122. package/lib/typescript/module/renderers/blockquote.d.ts.map +1 -1
  123. package/lib/typescript/module/renderers/code.d.ts +5 -5
  124. package/lib/typescript/module/renderers/code.d.ts.map +1 -1
  125. package/lib/typescript/module/renderers/heading.d.ts +3 -3
  126. package/lib/typescript/module/renderers/heading.d.ts.map +1 -1
  127. package/lib/typescript/module/renderers/horizontal-rule.d.ts +2 -2
  128. package/lib/typescript/module/renderers/horizontal-rule.d.ts.map +1 -1
  129. package/lib/typescript/module/renderers/image.d.ts +2 -2
  130. package/lib/typescript/module/renderers/image.d.ts.map +1 -1
  131. package/lib/typescript/module/renderers/link.d.ts +3 -3
  132. package/lib/typescript/module/renderers/link.d.ts.map +1 -1
  133. package/lib/typescript/module/renderers/list.d.ts +7 -7
  134. package/lib/typescript/module/renderers/list.d.ts.map +1 -1
  135. package/lib/typescript/module/renderers/math.d.ts +4 -4
  136. package/lib/typescript/module/renderers/math.d.ts.map +1 -1
  137. package/lib/typescript/module/renderers/paragraph.d.ts +3 -3
  138. package/lib/typescript/module/renderers/paragraph.d.ts.map +1 -1
  139. package/lib/typescript/module/renderers/table.d.ts +3 -3
  140. package/lib/typescript/module/renderers/table.d.ts.map +1 -1
  141. package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts +5 -2
  142. package/lib/typescript/module/specs/MarkdownSession.nitro.d.ts.map +1 -1
  143. package/lib/typescript/module/theme.d.ts +2 -2
  144. package/lib/typescript/module/theme.d.ts.map +1 -1
  145. package/lib/typescript/module/use-markdown-stream.d.ts.map +1 -1
  146. package/lib/typescript/module/utils/incremental-ast.d.ts +12 -0
  147. package/lib/typescript/module/utils/incremental-ast.d.ts.map +1 -0
  148. package/lib/typescript/module/utils/link-security.d.ts +3 -0
  149. package/lib/typescript/module/utils/link-security.d.ts.map +1 -0
  150. package/lib/typescript/module/utils/stream-timeline.d.ts +11 -0
  151. package/lib/typescript/module/utils/stream-timeline.d.ts.map +1 -0
  152. package/nitrogen/generated/android/NitroMarkdownOnLoad.cpp +2 -0
  153. package/nitrogen/generated/android/c++/JFunc_void_double_double.hpp +75 -0
  154. package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.cpp +18 -6
  155. package/nitrogen/generated/android/c++/JHybridMarkdownSessionSpec.hpp +4 -2
  156. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitromarkdown/Func_void_double_double.kt +80 -0
  157. package/nitrogen/generated/android/kotlin/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSessionSpec.kt +11 -3
  158. package/nitrogen/generated/ios/NitroMarkdown-Swift-Cxx-Bridge.cpp +8 -0
  159. package/nitrogen/generated/ios/NitroMarkdown-Swift-Cxx-Bridge.hpp +31 -0
  160. package/nitrogen/generated/ios/c++/HybridMarkdownSessionSpecSwift.hpp +20 -2
  161. package/nitrogen/generated/ios/swift/Func_void.swift +0 -1
  162. package/nitrogen/generated/ios/swift/Func_void_double_double.swift +46 -0
  163. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec.swift +4 -3
  164. package/nitrogen/generated/ios/swift/HybridMarkdownSessionSpec_cxx.swift +34 -10
  165. package/nitrogen/generated/shared/c++/HybridMarkdownParserSpec.cpp +2 -0
  166. package/nitrogen/generated/shared/c++/HybridMarkdownParserSpec.hpp +2 -0
  167. package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.cpp +2 -0
  168. package/nitrogen/generated/shared/c++/HybridMarkdownSessionSpec.hpp +4 -2
  169. package/package.json +7 -5
  170. package/src/Markdown.nitro.ts +7 -3
  171. package/src/MarkdownContext.ts +31 -25
  172. package/src/headless.ts +44 -6
  173. package/src/index.ts +8 -0
  174. package/src/markdown-stream.tsx +159 -15
  175. package/src/markdown.tsx +406 -50
  176. package/src/renderers/blockquote.tsx +4 -4
  177. package/src/renderers/code.tsx +16 -10
  178. package/src/renderers/heading.tsx +4 -4
  179. package/src/renderers/horizontal-rule.tsx +3 -3
  180. package/src/renderers/image.tsx +11 -9
  181. package/src/renderers/link.tsx +25 -7
  182. package/src/renderers/list.tsx +9 -12
  183. package/src/renderers/math.tsx +4 -4
  184. package/src/renderers/paragraph.tsx +3 -3
  185. package/src/renderers/table.tsx +270 -98
  186. package/src/specs/MarkdownSession.nitro.ts +6 -2
  187. package/src/theme.ts +3 -3
  188. package/src/use-markdown-stream.ts +22 -16
  189. package/src/utils/incremental-ast.ts +224 -0
  190. package/src/utils/link-security.ts +22 -0
  191. package/src/utils/stream-timeline.ts +72 -0
@@ -5,7 +5,6 @@
5
5
  /// Copyright © Marc Rousavy @ Margelo
6
6
  ///
7
7
 
8
- import Foundation
9
8
  import NitroModules
10
9
 
11
10
  /// See ``HybridMarkdownSessionSpec``
@@ -14,10 +13,12 @@ public protocol HybridMarkdownSessionSpec_protocol: HybridObject {
14
13
  var highlightPosition: Double { get set }
15
14
 
16
15
  // Methods
17
- func append(chunk: String) throws -> Void
16
+ func append(chunk: String) throws -> Double
18
17
  func clear() throws -> Void
19
18
  func getAllText() throws -> String
20
- func addListener(listener: @escaping () -> Void) throws -> () -> Void
19
+ func getLength() throws -> Double
20
+ func getTextRange(from: Double, to: Double) throws -> String
21
+ func addListener(listener: @escaping (_ from: Double, _ to: Double) -> Void) throws -> () -> Void
21
22
  }
22
23
 
23
24
  public extension HybridMarkdownSessionSpec_protocol {
@@ -5,7 +5,6 @@
5
5
  /// Copyright © Marc Rousavy @ Margelo
6
6
  ///
7
7
 
8
- import Foundation
9
8
  import NitroModules
10
9
 
11
10
  /**
@@ -135,13 +134,14 @@ open class HybridMarkdownSessionSpec_cxx {
135
134
 
136
135
  // Methods
137
136
  @inline(__always)
138
- public final func append(chunk: std.string) -> bridge.Result_void_ {
137
+ public final func append(chunk: std.string) -> bridge.Result_double_ {
139
138
  do {
140
- try self.__implementation.append(chunk: String(chunk))
141
- return bridge.create_Result_void_()
139
+ let __result = try self.__implementation.append(chunk: String(chunk))
140
+ let __resultCpp = __result
141
+ return bridge.create_Result_double_(__resultCpp)
142
142
  } catch (let __error) {
143
143
  let __exceptionPtr = __error.toCpp()
144
- return bridge.create_Result_void_(__exceptionPtr)
144
+ return bridge.create_Result_double_(__exceptionPtr)
145
145
  }
146
146
  }
147
147
 
@@ -169,12 +169,36 @@ open class HybridMarkdownSessionSpec_cxx {
169
169
  }
170
170
 
171
171
  @inline(__always)
172
- public final func addListener(listener: bridge.Func_void) -> bridge.Result_std__function_void____ {
172
+ public final func getLength() -> bridge.Result_double_ {
173
+ do {
174
+ let __result = try self.__implementation.getLength()
175
+ let __resultCpp = __result
176
+ return bridge.create_Result_double_(__resultCpp)
177
+ } catch (let __error) {
178
+ let __exceptionPtr = __error.toCpp()
179
+ return bridge.create_Result_double_(__exceptionPtr)
180
+ }
181
+ }
182
+
183
+ @inline(__always)
184
+ public final func getTextRange(from: Double, to: Double) -> bridge.Result_std__string_ {
185
+ do {
186
+ let __result = try self.__implementation.getTextRange(from: from, to: to)
187
+ let __resultCpp = std.string(__result)
188
+ return bridge.create_Result_std__string_(__resultCpp)
189
+ } catch (let __error) {
190
+ let __exceptionPtr = __error.toCpp()
191
+ return bridge.create_Result_std__string_(__exceptionPtr)
192
+ }
193
+ }
194
+
195
+ @inline(__always)
196
+ public final func addListener(listener: bridge.Func_void_double_double) -> bridge.Result_std__function_void____ {
173
197
  do {
174
- let __result = try self.__implementation.addListener(listener: { () -> () -> Void in
175
- let __wrappedFunction = bridge.wrap_Func_void(listener)
176
- return { () -> Void in
177
- __wrappedFunction.call()
198
+ let __result = try self.__implementation.addListener(listener: { () -> (Double, Double) -> Void in
199
+ let __wrappedFunction = bridge.wrap_Func_void_double_double(listener)
200
+ return { (__from: Double, __to: Double) -> Void in
201
+ __wrappedFunction.call(__from, __to)
178
202
  }
179
203
  }())
180
204
  let __resultCpp = { () -> bridge.Func_void in
@@ -16,6 +16,8 @@ namespace margelo::nitro::Markdown {
16
16
  registerHybrids(this, [](Prototype& prototype) {
17
17
  prototype.registerHybridMethod("parse", &HybridMarkdownParserSpec::parse);
18
18
  prototype.registerHybridMethod("parseWithOptions", &HybridMarkdownParserSpec::parseWithOptions);
19
+ prototype.registerHybridMethod("extractPlainText", &HybridMarkdownParserSpec::extractPlainText);
20
+ prototype.registerHybridMethod("extractPlainTextWithOptions", &HybridMarkdownParserSpec::extractPlainTextWithOptions);
19
21
  });
20
22
  }
21
23
 
@@ -52,6 +52,8 @@ namespace margelo::nitro::Markdown {
52
52
  // Methods
53
53
  virtual std::string parse(const std::string& text) = 0;
54
54
  virtual std::string parseWithOptions(const std::string& text, const ParserOptions& options) = 0;
55
+ virtual std::string extractPlainText(const std::string& text) = 0;
56
+ virtual std::string extractPlainTextWithOptions(const std::string& text, const ParserOptions& options) = 0;
55
57
 
56
58
  protected:
57
59
  // Hybrid Setup
@@ -19,6 +19,8 @@ namespace margelo::nitro::Markdown {
19
19
  prototype.registerHybridMethod("append", &HybridMarkdownSessionSpec::append);
20
20
  prototype.registerHybridMethod("clear", &HybridMarkdownSessionSpec::clear);
21
21
  prototype.registerHybridMethod("getAllText", &HybridMarkdownSessionSpec::getAllText);
22
+ prototype.registerHybridMethod("getLength", &HybridMarkdownSessionSpec::getLength);
23
+ prototype.registerHybridMethod("getTextRange", &HybridMarkdownSessionSpec::getTextRange);
22
24
  prototype.registerHybridMethod("addListener", &HybridMarkdownSessionSpec::addListener);
23
25
  });
24
26
  }
@@ -50,10 +50,12 @@ namespace margelo::nitro::Markdown {
50
50
 
51
51
  public:
52
52
  // Methods
53
- virtual void append(const std::string& chunk) = 0;
53
+ virtual double append(const std::string& chunk) = 0;
54
54
  virtual void clear() = 0;
55
55
  virtual std::string getAllText() = 0;
56
- virtual std::function<void()> addListener(const std::function<void()>& listener) = 0;
56
+ virtual double getLength() = 0;
57
+ virtual std::string getTextRange(double from, double to) = 0;
58
+ virtual std::function<void()> addListener(const std::function<void(double /* from */, double /* to */)>& listener) = 0;
57
59
 
58
60
  protected:
59
61
  // Hybrid Setup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-markdown",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "High-performance Markdown parser for React Native using Nitro Modules and md4c",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -50,10 +50,11 @@
50
50
  "!scripts"
51
51
  ],
52
52
  "scripts": {
53
- "prebuild": "npm run codegen",
53
+ "prebuild": "bun run codegen",
54
54
  "build": "bob build",
55
55
  "clean": "rimraf lib nitrogen/generated",
56
56
  "codegen": "nitrogen --logLevel=\"debug\"",
57
+ "lint": "eslint src --max-warnings=0 --ignore-pattern 'src/**/__tests__/**' --ignore-pattern 'src/**/*.nitro.ts'",
57
58
  "typecheck": "tsc --noEmit",
58
59
  "test": "jest",
59
60
  "test:coverage": "jest --coverage",
@@ -86,10 +87,11 @@
86
87
  "registry": "https://registry.npmjs.org/"
87
88
  },
88
89
  "devDependencies": {
89
- "@types/react": "^19.2.11",
90
+ "@types/react": "^19.2.14",
90
91
  "@types/react-native": "^0.73.0",
91
- "react-native-builder-bob": "^0.40.17",
92
- "react-native-nitro-modules": "*",
92
+ "@types/react-test-renderer": "^19.1.0",
93
+ "react-native-builder-bob": "^0.40.18",
94
+ "react-test-renderer": "^19.2.4",
93
95
  "typescript": "^5.9.3"
94
96
  },
95
97
  "peerDependencies": {
@@ -1,12 +1,16 @@
1
- import type { HybridObject } from 'react-native-nitro-modules';
1
+ import type { HybridObject } from "react-native-nitro-modules";
2
2
 
3
3
  export interface ParserOptions {
4
4
  gfm?: boolean;
5
5
  math?: boolean;
6
6
  }
7
7
 
8
- export interface MarkdownParser
9
- extends HybridObject<{ ios: 'c++'; android: 'c++' }> {
8
+ export interface MarkdownParser extends HybridObject<{
9
+ ios: "c++";
10
+ android: "c++";
11
+ }> {
10
12
  parse(text: string): string;
11
13
  parseWithOptions(text: string, options: ParserOptions): string;
14
+ extractPlainText(text: string): string;
15
+ extractPlainTextWithOptions(text: string, options: ParserOptions): string;
12
16
  }
@@ -4,28 +4,28 @@ import {
4
4
  type ReactNode,
5
5
  type ComponentType,
6
6
  } from "react";
7
+ import type { MarkdownNode } from "./headless";
7
8
  import {
8
9
  defaultMarkdownTheme,
9
10
  type MarkdownTheme,
10
11
  type NodeStyleOverrides,
11
12
  type StylingStrategy,
12
13
  } from "./theme";
13
- import type { MarkdownNode } from "./headless";
14
14
 
15
- export interface NodeRendererProps {
15
+ export type NodeRendererProps = {
16
16
  node: MarkdownNode;
17
17
  depth: number;
18
18
  inListItem: boolean;
19
19
  parentIsText?: boolean;
20
- }
20
+ };
21
21
 
22
- export interface BaseCustomRendererProps {
22
+ export type BaseCustomRendererProps = {
23
23
  node: MarkdownNode;
24
24
  children: ReactNode;
25
25
  Renderer: ComponentType<NodeRendererProps>;
26
- }
26
+ };
27
27
 
28
- export interface EnhancedRendererProps extends BaseCustomRendererProps {
28
+ export type EnhancedRendererProps = {
29
29
  level?: 1 | 2 | 3 | 4 | 5 | 6;
30
30
  href?: string;
31
31
  title?: string;
@@ -36,63 +36,69 @@ export interface EnhancedRendererProps extends BaseCustomRendererProps {
36
36
  ordered?: boolean;
37
37
  start?: number;
38
38
  checked?: boolean;
39
- }
39
+ } & BaseCustomRendererProps;
40
40
 
41
- export interface HeadingRendererProps extends BaseCustomRendererProps {
41
+ export type HeadingRendererProps = {
42
42
  level: 1 | 2 | 3 | 4 | 5 | 6;
43
- }
43
+ } & BaseCustomRendererProps;
44
44
 
45
- export interface LinkRendererProps extends BaseCustomRendererProps {
45
+ export type LinkRendererProps = {
46
46
  href: string;
47
47
  title?: string;
48
- }
48
+ } & BaseCustomRendererProps;
49
49
 
50
- export interface ImageRendererProps extends BaseCustomRendererProps {
50
+ export type ImageRendererProps = {
51
51
  url: string;
52
52
  alt?: string;
53
53
  title?: string;
54
- }
54
+ } & BaseCustomRendererProps;
55
55
 
56
- export interface CodeBlockRendererProps extends BaseCustomRendererProps {
56
+ export type CodeBlockRendererProps = {
57
57
  content: string;
58
58
  language?: string;
59
- }
59
+ } & BaseCustomRendererProps;
60
60
 
61
- export interface InlineCodeRendererProps extends BaseCustomRendererProps {
61
+ export type InlineCodeRendererProps = {
62
62
  content: string;
63
- }
63
+ } & BaseCustomRendererProps;
64
64
 
65
- export interface ListRendererProps extends BaseCustomRendererProps {
65
+ export type ListRendererProps = {
66
66
  ordered: boolean;
67
67
  start?: number;
68
- }
68
+ } & BaseCustomRendererProps;
69
69
 
70
- export interface TaskListItemRendererProps extends BaseCustomRendererProps {
70
+ export type TaskListItemRendererProps = {
71
71
  checked: boolean;
72
- }
72
+ } & BaseCustomRendererProps;
73
+
74
+ export type CustomRendererProps = {} & EnhancedRendererProps;
73
75
 
74
- export interface CustomRendererProps extends EnhancedRendererProps {}
76
+ export type LinkPressHandler = (
77
+ href: string,
78
+ ) => void | boolean | Promise<void | boolean>;
75
79
 
76
80
  export type CustomRenderer = (
77
- props: EnhancedRendererProps
81
+ props: EnhancedRendererProps,
78
82
  ) => ReactNode | undefined;
79
83
 
80
84
  export type CustomRenderers = Partial<
81
85
  Record<MarkdownNode["type"], CustomRenderer>
82
86
  >;
83
87
 
84
- export interface MarkdownContextValue {
88
+ export type MarkdownContextValue = {
85
89
  renderers: CustomRenderers;
86
90
  theme: MarkdownTheme;
87
91
  styles?: NodeStyleOverrides;
88
92
  stylingStrategy: StylingStrategy;
89
- }
93
+ onLinkPress?: LinkPressHandler;
94
+ };
90
95
 
91
96
  export const MarkdownContext = createContext<MarkdownContextValue>({
92
97
  renderers: {},
93
98
  theme: defaultMarkdownTheme,
94
99
  styles: undefined,
95
100
  stylingStrategy: "opinionated",
101
+ onLinkPress: undefined,
96
102
  });
97
103
 
98
104
  export const useMarkdownContext = () => useContext(MarkdownContext);
package/src/headless.ts CHANGED
@@ -19,7 +19,7 @@ export type { ParserOptions } from "./Markdown.nitro";
19
19
  * Represents a node in the Markdown AST (Abstract Syntax Tree).
20
20
  * Each node has a type and optional properties depending on the node type.
21
21
  */
22
- export interface MarkdownNode {
22
+ export type MarkdownNode = {
23
23
  /** The type of markdown element this node represents. Used to decide how to render the node. */
24
24
  type:
25
25
  | "document"
@@ -71,9 +71,13 @@ export interface MarkdownNode {
71
71
  isHeader?: boolean;
72
72
  /** Text alignment for table cells: 'left', 'center', or 'right'. */
73
73
  align?: string;
74
+ /** Source start offset in original markdown text (when provided by native parser). */
75
+ beg?: number;
76
+ /** Source end offset in original markdown text (when provided by native parser). */
77
+ end?: number;
74
78
  /** Nested child nodes for hierarchical elements like paragraphs, lists, and tables. */
75
79
  children?: MarkdownNode[];
76
- }
80
+ };
77
81
 
78
82
  export const MarkdownParserModule =
79
83
  NitroModules.createHybridObject<MarkdownParser>("MarkdownParser");
@@ -84,8 +88,12 @@ export const MarkdownParserModule =
84
88
  * @returns The root node of the parsed AST
85
89
  */
86
90
  export function parseMarkdown(text: string): MarkdownNode {
87
- const jsonStr = MarkdownParserModule.parse(text);
88
- return JSON.parse(jsonStr) as MarkdownNode;
91
+ if (typeof MarkdownParserModule.parse === "function") {
92
+ const jsonStr = MarkdownParserModule.parse(text);
93
+ return JSON.parse(jsonStr) as MarkdownNode;
94
+ }
95
+
96
+ return { type: "document", children: [] };
89
97
  }
90
98
 
91
99
  /**
@@ -98,8 +106,38 @@ export function parseMarkdownWithOptions(
98
106
  text: string,
99
107
  options: ParserOptions,
100
108
  ): MarkdownNode {
101
- const jsonStr = MarkdownParserModule.parseWithOptions(text, options);
102
- return JSON.parse(jsonStr) as MarkdownNode;
109
+ if (typeof MarkdownParserModule.parseWithOptions === "function") {
110
+ const jsonStr = MarkdownParserModule.parseWithOptions(text, options);
111
+ return JSON.parse(jsonStr) as MarkdownNode;
112
+ }
113
+
114
+ return { type: "document", children: [] };
115
+ }
116
+
117
+ /**
118
+ * Parse markdown and return flattened plain text directly from native parser.
119
+ * Useful for search/indexing flows that don't need full AST rendering.
120
+ */
121
+ export function extractPlainText(text: string): string {
122
+ if (typeof MarkdownParserModule.extractPlainText === "function") {
123
+ return MarkdownParserModule.extractPlainText(text);
124
+ }
125
+
126
+ return getFlattenedText(parseMarkdown(text));
127
+ }
128
+
129
+ /**
130
+ * Parse markdown with options and return flattened plain text from native parser.
131
+ */
132
+ export function extractPlainTextWithOptions(
133
+ text: string,
134
+ options: ParserOptions,
135
+ ): string {
136
+ if (typeof MarkdownParserModule.extractPlainTextWithOptions === "function") {
137
+ return MarkdownParserModule.extractPlainTextWithOptions(text, options);
138
+ }
139
+
140
+ return getFlattenedText(parseMarkdownWithOptions(text, options));
103
141
  }
104
142
 
105
143
  export type { MarkdownParser };
package/src/index.ts CHANGED
@@ -1,7 +1,14 @@
1
1
  export * from "./headless";
2
2
 
3
3
  export { Markdown } from "./markdown";
4
+ export type {
5
+ MarkdownProps,
6
+ AstTransform,
7
+ MarkdownPlugin,
8
+ MarkdownVirtualizationOptions,
9
+ } from "./markdown";
4
10
  export { MarkdownStream } from "./markdown-stream";
11
+ export type { MarkdownStreamProps } from "./markdown-stream";
5
12
 
6
13
  export { useMarkdownContext, MarkdownContext } from "./MarkdownContext";
7
14
  export type {
@@ -18,6 +25,7 @@ export type {
18
25
  InlineCodeRendererProps,
19
26
  ListRendererProps,
20
27
  TaskListItemRendererProps,
28
+ LinkPressHandler,
21
29
  MarkdownContextValue,
22
30
  } from "./MarkdownContext";
23
31
 
@@ -2,13 +2,55 @@ import {
2
2
  useState,
3
3
  useEffect,
4
4
  useRef,
5
+ useCallback,
5
6
  startTransition,
6
7
  type FC,
7
8
  } from "react";
9
+ import type { MarkdownNode } from "./headless";
8
10
  import { Markdown, type MarkdownProps } from "./markdown";
9
11
  import type { MarkdownSession } from "./specs/MarkdownSession.nitro";
12
+ import { getNextStreamAst, parseMarkdownAst } from "./utils/incremental-ast";
10
13
 
11
- export interface MarkdownStreamProps extends Omit<MarkdownProps, "children"> {
14
+ const normalizeOffset = (value: number): number | null => {
15
+ if (!Number.isFinite(value)) return null;
16
+ if (value <= 0) return 0;
17
+ return Math.floor(value);
18
+ };
19
+
20
+ const resolveStreamText = ({
21
+ forceFullSync,
22
+ pendingFrom,
23
+ pendingTo,
24
+ previousText,
25
+ session,
26
+ }: {
27
+ forceFullSync: boolean;
28
+ pendingFrom: number | null;
29
+ pendingTo: number | null;
30
+ previousText: string;
31
+ session: MarkdownSession;
32
+ }): string => {
33
+ if (forceFullSync || pendingFrom === null || pendingTo === null) {
34
+ return session.getAllText();
35
+ }
36
+
37
+ if (pendingTo < pendingFrom) {
38
+ return session.getAllText();
39
+ }
40
+
41
+ if (pendingFrom === previousText.length) {
42
+ const appendedChunk = session.getTextRange(pendingFrom, pendingTo);
43
+ return `${previousText}${appendedChunk}`;
44
+ }
45
+
46
+ if (pendingFrom === 0) {
47
+ return session.getTextRange(0, pendingTo);
48
+ }
49
+
50
+ return session.getAllText();
51
+ };
52
+
53
+ export type MarkdownStreamProps = {
12
54
  /**
13
55
  * The active MarkdownSession to stream content from.
14
56
  */
@@ -29,7 +71,12 @@ export interface MarkdownStreamProps extends Omit<MarkdownProps, "children"> {
29
71
  * Useful when you want to prioritize user interactions over stream renders.
30
72
  */
31
73
  useTransitionUpdates?: boolean;
32
- }
74
+ /**
75
+ * Enable incremental AST updates for append-only streams.
76
+ * Automatically falls back to full parse when updates are not safely mergeable.
77
+ */
78
+ incrementalParsing?: boolean;
79
+ } & Omit<MarkdownProps, "children" | "sourceAst">;
33
80
 
34
81
  /**
35
82
  * A component that renders streaming Markdown from a MarkdownSession.
@@ -40,20 +87,55 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
40
87
  updateIntervalMs = 50,
41
88
  updateStrategy = "interval",
42
89
  useTransitionUpdates = false,
90
+ incrementalParsing = true,
91
+ options,
92
+ plugins,
43
93
  ...props
44
94
  }) => {
45
- const [text, setText] = useState(() => session.getAllText());
95
+ const parseText = useCallback(
96
+ (text: string): MarkdownNode => parseMarkdownAst(text, options),
97
+ [options],
98
+ );
99
+ const createEmptyAst = (): MarkdownNode => ({
100
+ type: "document",
101
+ children: [],
102
+ });
103
+ const initialText = session.getAllText();
104
+ const hasBeforeParsePlugins =
105
+ plugins?.some((plugin) => typeof plugin.beforeParse === "function") ??
106
+ false;
107
+ const [renderState, setRenderState] = useState(() => ({
108
+ text: initialText,
109
+ ast: hasBeforeParsePlugins ? createEmptyAst() : parseText(initialText),
110
+ }));
111
+ const renderStateRef = useRef(renderState);
46
112
  const pendingUpdateRef = useRef(false);
113
+ const pendingFromRef = useRef<number | null>(null);
114
+ const pendingToRef = useRef<number | null>(null);
115
+ const forceFullSyncRef = useRef(false);
47
116
  const updateTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
48
117
  const rafRef = useRef<number | null>(null);
49
- const lastEmittedRef = useRef(text);
118
+ const allowIncremental = incrementalParsing && !hasBeforeParsePlugins;
119
+
120
+ useEffect(() => {
121
+ renderStateRef.current = renderState;
122
+ }, [renderState]);
50
123
 
51
124
  useEffect(() => {
52
- // Ensure initial state is synced
53
125
  const initialText = session.getAllText();
54
- setText(initialText);
55
- lastEmittedRef.current = initialText;
126
+ const initialState = {
127
+ text: initialText,
128
+ ast: hasBeforeParsePlugins ? createEmptyAst() : parseText(initialText),
129
+ };
130
+ pendingUpdateRef.current = false;
131
+ pendingFromRef.current = null;
132
+ pendingToRef.current = null;
133
+ forceFullSyncRef.current = false;
134
+ renderStateRef.current = initialState;
135
+ setRenderState(initialState);
136
+ }, [hasBeforeParsePlugins, parseText, session]);
56
137
 
138
+ useEffect(() => {
57
139
  const flushUpdate = () => {
58
140
  updateTimerRef.current = null;
59
141
  if (rafRef.current !== null) {
@@ -63,14 +145,44 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
63
145
  if (!pendingUpdateRef.current) return;
64
146
  pendingUpdateRef.current = false;
65
147
 
66
- const latest = session.getAllText();
67
- if (latest === lastEmittedRef.current) return;
68
- lastEmittedRef.current = latest;
148
+ const previousState = renderStateRef.current;
149
+ const pendingFrom = pendingFromRef.current;
150
+ const pendingTo = pendingToRef.current;
151
+ const forceFullSync = forceFullSyncRef.current;
152
+ pendingFromRef.current = null;
153
+ pendingToRef.current = null;
154
+ forceFullSyncRef.current = false;
155
+
156
+ const latest = resolveStreamText({
157
+ forceFullSync,
158
+ pendingFrom,
159
+ pendingTo,
160
+ previousText: previousState.text,
161
+ session,
162
+ });
163
+ if (latest === previousState.text) return;
164
+
165
+ const nextAst = hasBeforeParsePlugins
166
+ ? previousState.ast
167
+ : getNextStreamAst({
168
+ allowIncremental,
169
+ nextText: latest,
170
+ options,
171
+ previousAst: previousState.ast,
172
+ previousText: previousState.text,
173
+ });
174
+ const nextState = {
175
+ text: latest,
176
+ ast: nextAst,
177
+ };
178
+ renderStateRef.current = nextState;
69
179
 
70
180
  if (useTransitionUpdates) {
71
- startTransition(() => setText(latest));
181
+ startTransition(() => {
182
+ setRenderState(nextState);
183
+ });
72
184
  } else {
73
- setText(latest);
185
+ setRenderState(nextState);
74
186
  }
75
187
  };
76
188
 
@@ -87,7 +199,22 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
87
199
  }
88
200
  };
89
201
 
90
- const unsubscribe = session.addListener(() => {
202
+ const unsubscribe = session.addListener((from, to) => {
203
+ const nextFrom = normalizeOffset(from);
204
+ const nextTo = normalizeOffset(to);
205
+
206
+ if (nextFrom === null || nextTo === null || nextTo < nextFrom) {
207
+ forceFullSyncRef.current = true;
208
+ } else {
209
+ const currentFrom = pendingFromRef.current;
210
+ const currentTo = pendingToRef.current;
211
+
212
+ pendingFromRef.current =
213
+ currentFrom === null ? nextFrom : Math.min(currentFrom, nextFrom);
214
+ pendingToRef.current =
215
+ currentTo === null ? nextTo : Math.max(currentTo, nextTo);
216
+ }
217
+
91
218
  pendingUpdateRef.current = true;
92
219
  scheduleFlush();
93
220
  });
@@ -103,7 +230,24 @@ export const MarkdownStream: FC<MarkdownStreamProps> = ({
103
230
  rafRef.current = null;
104
231
  }
105
232
  };
106
- }, [session, updateIntervalMs, updateStrategy, useTransitionUpdates]);
233
+ }, [
234
+ allowIncremental,
235
+ hasBeforeParsePlugins,
236
+ options,
237
+ session,
238
+ updateIntervalMs,
239
+ updateStrategy,
240
+ useTransitionUpdates,
241
+ ]);
107
242
 
108
- return <Markdown {...props}>{text}</Markdown>;
243
+ return (
244
+ <Markdown
245
+ {...props}
246
+ options={options}
247
+ plugins={plugins}
248
+ sourceAst={hasBeforeParsePlugins ? undefined : renderState.ast}
249
+ >
250
+ {renderState.text}
251
+ </Markdown>
252
+ );
109
253
  };