react-native-nitro-markdown 0.5.1 → 0.5.3

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 (116) hide show
  1. package/README.md +257 -682
  2. package/android/CMakeLists.txt +8 -1
  3. package/android/build.gradle +9 -2
  4. package/android/consumer-rules.pro +31 -0
  5. package/android/gradle.properties +2 -0
  6. package/android/src/main/cpp/cpp-adapter.cpp +4 -1
  7. package/android/src/main/java/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSession.kt +61 -21
  8. package/android/src/main/java/com/nitromarkdown/NitroMarkdownPackage.kt +6 -18
  9. package/cpp/bindings/HybridMarkdownParser.cpp +38 -12
  10. package/cpp/bindings/HybridMarkdownParser.hpp +4 -4
  11. package/cpp/bindings/HybridMarkdownSession.cpp +2 -0
  12. package/cpp/core/MD4CParser.cpp +128 -85
  13. package/cpp/core/MarkdownSessionCore.cpp +2 -0
  14. package/ios/HybridMarkdownSession.swift +89 -46
  15. package/lib/commonjs/headless.js +33 -7
  16. package/lib/commonjs/headless.js.map +1 -1
  17. package/lib/commonjs/index.js +48 -38
  18. package/lib/commonjs/index.js.map +1 -1
  19. package/lib/commonjs/markdown-stream.js +1 -1
  20. package/lib/commonjs/markdown-stream.js.map +1 -1
  21. package/lib/commonjs/markdown.js +47 -10
  22. package/lib/commonjs/markdown.js.map +1 -1
  23. package/lib/commonjs/renderers/code.js +1 -1
  24. package/lib/commonjs/renderers/code.js.map +1 -1
  25. package/lib/commonjs/renderers/image.js +6 -1
  26. package/lib/commonjs/renderers/image.js.map +1 -1
  27. package/lib/commonjs/renderers/link.js +7 -2
  28. package/lib/commonjs/renderers/link.js.map +1 -1
  29. package/lib/commonjs/renderers/list.js +2 -0
  30. package/lib/commonjs/renderers/list.js.map +1 -1
  31. package/lib/commonjs/renderers/math.js +4 -2
  32. package/lib/commonjs/renderers/math.js.map +1 -1
  33. package/lib/commonjs/renderers/table/cell-content.js +1 -1
  34. package/lib/commonjs/renderers/table/cell-content.js.map +1 -1
  35. package/lib/commonjs/renderers/table/index.js +10 -2
  36. package/lib/commonjs/renderers/table/index.js.map +1 -1
  37. package/lib/commonjs/theme.js +7 -7
  38. package/lib/commonjs/theme.js.map +1 -1
  39. package/lib/commonjs/utils/code-highlight.js +24 -25
  40. package/lib/commonjs/utils/code-highlight.js.map +1 -1
  41. package/lib/module/headless.js +34 -6
  42. package/lib/module/headless.js.map +1 -1
  43. package/lib/module/index.js +1 -1
  44. package/lib/module/index.js.map +1 -1
  45. package/lib/module/markdown-stream.js +1 -1
  46. package/lib/module/markdown-stream.js.map +1 -1
  47. package/lib/module/markdown.js +48 -11
  48. package/lib/module/markdown.js.map +1 -1
  49. package/lib/module/renderers/code.js +1 -1
  50. package/lib/module/renderers/code.js.map +1 -1
  51. package/lib/module/renderers/image.js +6 -1
  52. package/lib/module/renderers/image.js.map +1 -1
  53. package/lib/module/renderers/link.js +7 -2
  54. package/lib/module/renderers/link.js.map +1 -1
  55. package/lib/module/renderers/list.js +2 -0
  56. package/lib/module/renderers/list.js.map +1 -1
  57. package/lib/module/renderers/math.js +4 -2
  58. package/lib/module/renderers/math.js.map +1 -1
  59. package/lib/module/renderers/table/cell-content.js +1 -1
  60. package/lib/module/renderers/table/cell-content.js.map +1 -1
  61. package/lib/module/renderers/table/index.js +10 -2
  62. package/lib/module/renderers/table/index.js.map +1 -1
  63. package/lib/module/theme.js +7 -7
  64. package/lib/module/theme.js.map +1 -1
  65. package/lib/module/utils/code-highlight.js +24 -25
  66. package/lib/module/utils/code-highlight.js.map +1 -1
  67. package/lib/typescript/commonjs/headless.d.ts +9 -1
  68. package/lib/typescript/commonjs/headless.d.ts.map +1 -1
  69. package/lib/typescript/commonjs/index.d.ts +3 -2
  70. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  71. package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
  72. package/lib/typescript/commonjs/markdown.d.ts +7 -2
  73. package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
  75. package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/renderers/link.d.ts.map +1 -1
  77. package/lib/typescript/commonjs/renderers/list.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
  79. package/lib/typescript/commonjs/renderers/table/cell-content.d.ts +4 -3
  80. package/lib/typescript/commonjs/renderers/table/cell-content.d.ts.map +1 -1
  81. package/lib/typescript/commonjs/renderers/table/index.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/theme.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/utils/code-highlight.d.ts +1 -1
  84. package/lib/typescript/commonjs/utils/code-highlight.d.ts.map +1 -1
  85. package/lib/typescript/module/headless.d.ts +9 -1
  86. package/lib/typescript/module/headless.d.ts.map +1 -1
  87. package/lib/typescript/module/index.d.ts +3 -2
  88. package/lib/typescript/module/index.d.ts.map +1 -1
  89. package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
  90. package/lib/typescript/module/markdown.d.ts +7 -2
  91. package/lib/typescript/module/markdown.d.ts.map +1 -1
  92. package/lib/typescript/module/renderers/code.d.ts.map +1 -1
  93. package/lib/typescript/module/renderers/image.d.ts.map +1 -1
  94. package/lib/typescript/module/renderers/link.d.ts.map +1 -1
  95. package/lib/typescript/module/renderers/list.d.ts.map +1 -1
  96. package/lib/typescript/module/renderers/math.d.ts.map +1 -1
  97. package/lib/typescript/module/renderers/table/cell-content.d.ts +4 -3
  98. package/lib/typescript/module/renderers/table/cell-content.d.ts.map +1 -1
  99. package/lib/typescript/module/renderers/table/index.d.ts.map +1 -1
  100. package/lib/typescript/module/theme.d.ts.map +1 -1
  101. package/lib/typescript/module/utils/code-highlight.d.ts +1 -1
  102. package/lib/typescript/module/utils/code-highlight.d.ts.map +1 -1
  103. package/package.json +5 -3
  104. package/src/headless.ts +57 -7
  105. package/src/index.ts +16 -2
  106. package/src/markdown-stream.tsx +1 -0
  107. package/src/markdown.tsx +98 -31
  108. package/src/renderers/code.tsx +23 -16
  109. package/src/renderers/image.tsx +9 -1
  110. package/src/renderers/link.tsx +8 -2
  111. package/src/renderers/list.tsx +2 -0
  112. package/src/renderers/math.tsx +6 -2
  113. package/src/renderers/table/cell-content.tsx +15 -4
  114. package/src/renderers/table/index.tsx +15 -3
  115. package/src/theme.ts +34 -14
  116. package/src/utils/code-highlight.ts +133 -44
@@ -15,6 +15,18 @@ size_t clampInputSize(size_t inputSize) {
15
15
  }
16
16
  return inputSize;
17
17
  }
18
+
19
+ // Safe pointer offset calculation — guards against out-of-allocation arithmetic.
20
+ // md4c callbacks receive pointers into the input buffer, so arithmetic is valid
21
+ // as long as the input string is stable. This check catches any edge cases.
22
+ static MD_OFFSET safeOffset(const char* text, const char* base, size_t baseSize) noexcept {
23
+ if (text < base) return 0;
24
+ ptrdiff_t diff = text - base;
25
+ if (diff < 0 || static_cast<size_t>(diff) > baseSize) return 0;
26
+ // Check MD_OFFSET won't truncate
27
+ if (static_cast<size_t>(diff) > static_cast<size_t>(std::numeric_limits<MD_OFFSET>::max())) return 0;
28
+ return static_cast<MD_OFFSET>(diff);
29
+ }
18
30
  } // namespace
19
31
 
20
32
  class MD4CParser::Impl {
@@ -38,13 +50,21 @@ public:
38
50
  }
39
51
 
40
52
  void flushText() {
41
- if (!currentText.empty() && !nodeStack.empty()) {
42
- auto textNode = std::make_shared<MarkdownNode>(NodeType::Text);
43
- textNode->content = std::move(currentText);
44
- textNode->beg = currentTextBeg;
45
- textNode->end = lastTextEnd;
46
- nodeStack.top()->addChild(std::move(textNode));
47
- currentText.clear();
53
+ if (!currentText.empty()) {
54
+ if (!nodeStack.empty()) {
55
+ auto textNode = std::make_shared<MarkdownNode>(NodeType::Text);
56
+ textNode->content = std::move(currentText);
57
+ textNode->beg = currentTextBeg;
58
+ textNode->end = lastTextEnd;
59
+ nodeStack.top()->addChild(std::move(textNode));
60
+ currentText.clear();
61
+ } else {
62
+ #if defined(NITROMARKDOWN_DEBUG) || defined(DEBUG)
63
+ // This indicates a parser state bug - text available but no node to attach it to
64
+ fprintf(stderr, "[NitroMarkdown] Warning: flushText called with empty nodeStack, text dropped: %.50s\n", currentText.c_str());
65
+ #endif
66
+ currentText.clear();
67
+ }
48
68
  }
49
69
  }
50
70
 
@@ -74,17 +94,18 @@ public:
74
94
  std::string result;
75
95
  result.reserve(attr->size);
76
96
 
77
- for (unsigned i = 0; ; i++) {
97
+ // md4c invariant: substr_types is terminated by an entry where
98
+ // substr_offsets[i] == attr->size (the sentinel entry). Reading
99
+ // substr_offsets[i+1] is always valid when substr_offsets[i] < attr->size.
100
+ for (unsigned i = 0; attr->substr_offsets[i] < attr->size; i++) {
78
101
  size_t start = static_cast<size_t>(attr->substr_offsets[i]);
79
- size_t end = static_cast<size_t>(attr->substr_offsets[i + 1]);
102
+ size_t end = static_cast<size_t>(attr->substr_offsets[i + 1]); // safe: [i+1] always valid when [i] < size
80
103
 
81
- if (end > attr->size) {
104
+ if (end > static_cast<size_t>(attr->size)) {
82
105
  end = static_cast<size_t>(attr->size);
83
106
  }
84
- if (start > end) {
85
- break;
86
- }
87
107
 
108
+ // Append content for all recognised text types
88
109
  if (attr->substr_types[i] == MD_TEXT_NORMAL ||
89
110
  attr->substr_types[i] == MD_TEXT_ENTITY ||
90
111
  attr->substr_types[i] == MD_TEXT_NULLCHAR) {
@@ -92,12 +113,11 @@ public:
92
113
  result.append(attr->text + start, end - start);
93
114
  }
94
115
  }
95
-
96
- if (end >= attr->size) {
97
- break;
98
- }
99
116
  }
100
117
 
118
+ // Fallback: if all substrings had unrecognised types (should not occur
119
+ // per the md4c spec, but guards against future spec extensions), return
120
+ // the raw attribute text.
101
121
  if (result.empty() && attr->size > 0) {
102
122
  result.assign(attr->text, attr->size);
103
123
  }
@@ -105,25 +125,27 @@ public:
105
125
  return result;
106
126
  }
107
127
 
108
- static int enterBlock(MD_BLOCKTYPE type, void* detail, MD_OFFSET off, void* userdata) {
128
+ static int enterBlock(MD_BLOCKTYPE type, void* detail, MD_OFFSET off, void* userdata) noexcept {
129
+ try {
109
130
  auto* impl = static_cast<Impl*>(userdata);
110
-
131
+ if (impl == nullptr) return 1; // Signal error to md4c
132
+
111
133
  switch (type) {
112
134
  case MD_BLOCK_DOC:
113
135
  break;
114
-
136
+
115
137
  case MD_BLOCK_QUOTE: {
116
138
  impl->pushNode(std::make_shared<MarkdownNode>(NodeType::Blockquote), off);
117
139
  break;
118
140
  }
119
-
141
+
120
142
  case MD_BLOCK_UL: {
121
143
  auto node = std::make_shared<MarkdownNode>(NodeType::List);
122
144
  node->ordered = false;
123
145
  impl->pushNode(node, off);
124
146
  break;
125
147
  }
126
-
148
+
127
149
  case MD_BLOCK_OL: {
128
150
  auto* d = static_cast<MD_BLOCK_OL_DETAIL*>(detail);
129
151
  auto node = std::make_shared<MarkdownNode>(NodeType::List);
@@ -132,7 +154,7 @@ public:
132
154
  impl->pushNode(node, off);
133
155
  break;
134
156
  }
135
-
157
+
136
158
  case MD_BLOCK_LI: {
137
159
  auto* d = static_cast<MD_BLOCK_LI_DETAIL*>(detail);
138
160
  if (d->is_task) {
@@ -144,12 +166,12 @@ public:
144
166
  }
145
167
  break;
146
168
  }
147
-
169
+
148
170
  case MD_BLOCK_HR: {
149
171
  impl->pushNode(std::make_shared<MarkdownNode>(NodeType::HorizontalRule), off);
150
172
  break;
151
173
  }
152
-
174
+
153
175
  case MD_BLOCK_H: {
154
176
  auto* d = static_cast<MD_BLOCK_H_DETAIL*>(detail);
155
177
  auto node = std::make_shared<MarkdownNode>(NodeType::Heading);
@@ -157,7 +179,7 @@ public:
157
179
  impl->pushNode(node, off);
158
180
  break;
159
181
  }
160
-
182
+
161
183
  case MD_BLOCK_CODE: {
162
184
  auto* d = static_cast<MD_BLOCK_CODE_DETAIL*>(detail);
163
185
  auto node = std::make_shared<MarkdownNode>(NodeType::CodeBlock);
@@ -167,37 +189,37 @@ public:
167
189
  impl->pushNode(node, off);
168
190
  break;
169
191
  }
170
-
192
+
171
193
  case MD_BLOCK_HTML: {
172
194
  impl->pushNode(std::make_shared<MarkdownNode>(NodeType::HtmlBlock), off);
173
195
  break;
174
196
  }
175
-
197
+
176
198
  case MD_BLOCK_P: {
177
199
  impl->pushNode(std::make_shared<MarkdownNode>(NodeType::Paragraph), off);
178
200
  break;
179
201
  }
180
-
202
+
181
203
  case MD_BLOCK_TABLE: {
182
204
  impl->pushNode(std::make_shared<MarkdownNode>(NodeType::Table), off);
183
205
  break;
184
206
  }
185
-
207
+
186
208
  case MD_BLOCK_THEAD: {
187
209
  impl->pushNode(std::make_shared<MarkdownNode>(NodeType::TableHead), off);
188
210
  break;
189
211
  }
190
-
212
+
191
213
  case MD_BLOCK_TBODY: {
192
214
  impl->pushNode(std::make_shared<MarkdownNode>(NodeType::TableBody), off);
193
215
  break;
194
216
  }
195
-
217
+
196
218
  case MD_BLOCK_TR: {
197
219
  impl->pushNode(std::make_shared<MarkdownNode>(NodeType::TableRow), off);
198
220
  break;
199
221
  }
200
-
222
+
201
223
  case MD_BLOCK_TH: {
202
224
  auto* d = static_cast<MD_BLOCK_TD_DETAIL*>(detail);
203
225
  auto node = std::make_shared<MarkdownNode>(NodeType::TableCell);
@@ -211,7 +233,7 @@ public:
211
233
  impl->pushNode(node, off);
212
234
  break;
213
235
  }
214
-
236
+
215
237
  case MD_BLOCK_TD: {
216
238
  auto* d = static_cast<MD_BLOCK_TD_DETAIL*>(detail);
217
239
  auto node = std::make_shared<MarkdownNode>(NodeType::TableCell);
@@ -226,14 +248,18 @@ public:
226
248
  break;
227
249
  }
228
250
  }
229
-
251
+
230
252
  return 0;
253
+ } catch (...) {
254
+ return 1; // Signal error to md4c
255
+ }
231
256
  }
232
257
 
233
- static int leaveBlock(MD_BLOCKTYPE type, void* detail, MD_OFFSET off, void* userdata) {
234
- (void)detail;
258
+ static int leaveBlock(MD_BLOCKTYPE type, [[maybe_unused]] void* detail, MD_OFFSET off, void* userdata) noexcept {
259
+ try {
235
260
  auto* impl = static_cast<Impl*>(userdata);
236
-
261
+ if (impl == nullptr) return 1; // Signal error to md4c
262
+
237
263
  switch (type) {
238
264
  case MD_BLOCK_DOC:
239
265
  impl->root->end = off;
@@ -245,29 +271,34 @@ public:
245
271
  impl->popNode(off);
246
272
  break;
247
273
  }
248
-
274
+
249
275
  return 0;
276
+ } catch (...) {
277
+ return 1; // Signal error to md4c
278
+ }
250
279
  }
251
280
 
252
- static int enterSpan(MD_SPANTYPE type, void* detail, MD_OFFSET off, void* userdata) {
281
+ static int enterSpan(MD_SPANTYPE type, void* detail, MD_OFFSET off, void* userdata) noexcept {
282
+ try {
253
283
  auto* impl = static_cast<Impl*>(userdata);
254
-
284
+ if (impl == nullptr) return 1; // Signal error to md4c
285
+
255
286
  switch (type) {
256
287
  case MD_SPAN_EM: {
257
288
  impl->pushNode(std::make_shared<MarkdownNode>(NodeType::Italic), off);
258
289
  break;
259
290
  }
260
-
291
+
261
292
  case MD_SPAN_STRONG: {
262
293
  impl->pushNode(std::make_shared<MarkdownNode>(NodeType::Bold), off);
263
294
  break;
264
295
  }
265
-
296
+
266
297
  case MD_SPAN_DEL: {
267
298
  impl->pushNode(std::make_shared<MarkdownNode>(NodeType::Strikethrough), off);
268
299
  break;
269
300
  }
270
-
301
+
271
302
  case MD_SPAN_A: {
272
303
  auto* d = static_cast<MD_SPAN_A_DETAIL*>(detail);
273
304
  auto node = std::make_shared<MarkdownNode>(NodeType::Link);
@@ -280,7 +311,7 @@ public:
280
311
  impl->pushNode(node, off);
281
312
  break;
282
313
  }
283
-
314
+
284
315
  case MD_SPAN_IMG: {
285
316
  auto* d = static_cast<MD_SPAN_IMG_DETAIL*>(detail);
286
317
  auto node = std::make_shared<MarkdownNode>(NodeType::Image);
@@ -293,40 +324,44 @@ public:
293
324
  impl->pushNode(node, off);
294
325
  break;
295
326
  }
296
-
327
+
297
328
  case MD_SPAN_CODE: {
298
329
  impl->pushNode(std::make_shared<MarkdownNode>(NodeType::CodeInline), off);
299
330
  break;
300
331
  }
301
-
332
+
302
333
  case MD_SPAN_LATEXMATH: {
303
334
  impl->pushNode(std::make_shared<MarkdownNode>(NodeType::MathInline), off);
304
335
  break;
305
336
  }
306
-
337
+
307
338
  case MD_SPAN_LATEXMATH_DISPLAY: {
308
339
  impl->pushNode(std::make_shared<MarkdownNode>(NodeType::MathBlock), off);
309
340
  break;
310
341
  }
311
-
342
+
312
343
  case MD_SPAN_U: {
313
344
  impl->pushNode(std::make_shared<MarkdownNode>(NodeType::Italic), off);
314
345
  break;
315
346
  }
316
-
347
+
317
348
  case MD_SPAN_WIKILINK: {
318
349
  auto node = std::make_shared<MarkdownNode>(NodeType::Link);
319
350
  impl->pushNode(node, off);
320
351
  break;
321
352
  }
322
353
  }
323
-
354
+
324
355
  return 0;
356
+ } catch (...) {
357
+ return 1; // Signal error to md4c
358
+ }
325
359
  }
326
360
 
327
- static int leaveSpan(MD_SPANTYPE type, void* detail, MD_OFFSET off, void* userdata) {
328
- (void)detail;
361
+ static int leaveSpan(MD_SPANTYPE type, [[maybe_unused]] void* detail, MD_OFFSET off, void* userdata) noexcept {
362
+ try {
329
363
  auto* impl = static_cast<Impl*>(userdata);
364
+ if (impl == nullptr) return 1; // Signal error to md4c
330
365
 
331
366
  if (!impl->nodeStack.empty()) {
332
367
  auto currentNode = impl->nodeStack.top();
@@ -349,71 +384,71 @@ public:
349
384
 
350
385
  impl->popNode(off);
351
386
  return 0;
387
+ } catch (...) {
388
+ return 1; // Signal error to md4c
389
+ }
352
390
  }
353
391
 
354
- static int text(MD_TEXTTYPE type, const MD_CHAR* text, MD_SIZE size, void* userdata) {
392
+ static int text(MD_TEXTTYPE type, const MD_CHAR* text, MD_SIZE size, void* userdata) noexcept {
393
+ try {
355
394
  auto* impl = static_cast<Impl*>(userdata);
395
+ if (impl == nullptr) return 1; // Signal error to md4c
356
396
 
357
397
  if (!text || size == 0) return 0;
358
398
 
359
399
  switch (type) {
360
400
  case MD_TEXT_NULLCHAR: {
361
- MD_OFFSET off = impl->lastTextEnd;
362
- ptrdiff_t diff = text - impl->inputText;
363
- if (diff >= 0 && static_cast<size_t>(diff) <= impl->inputTextSize) {
364
- off = static_cast<MD_OFFSET>(diff);
365
- }
401
+ MD_OFFSET off = safeOffset(text, impl->inputText, impl->inputTextSize);
402
+ if (off == 0 && text != impl->inputText) off = impl->lastTextEnd;
366
403
  if (impl->currentText.empty()) impl->currentTextBeg = off;
367
404
  impl->currentText += '\0';
368
405
  impl->lastTextEnd = off + 1;
369
406
  break;
370
407
  }
371
-
408
+
372
409
  case MD_TEXT_BR:
373
410
  impl->flushText();
374
- impl->nodeStack.top()->addChild(
375
- std::make_shared<MarkdownNode>(NodeType::LineBreak));
411
+ if (!impl->nodeStack.empty()) {
412
+ impl->nodeStack.top()->addChild(
413
+ std::make_shared<MarkdownNode>(NodeType::LineBreak));
414
+ }
376
415
  break;
377
-
416
+
378
417
  case MD_TEXT_SOFTBR:
379
418
  impl->flushText();
380
- impl->nodeStack.top()->addChild(
381
- std::make_shared<MarkdownNode>(NodeType::SoftBreak));
419
+ if (!impl->nodeStack.empty()) {
420
+ impl->nodeStack.top()->addChild(
421
+ std::make_shared<MarkdownNode>(NodeType::SoftBreak));
422
+ }
382
423
  break;
383
-
424
+
384
425
  case MD_TEXT_HTML:
385
426
  impl->flushText();
386
- {
427
+ if (!impl->nodeStack.empty()) {
387
428
  auto node = std::make_shared<MarkdownNode>(NodeType::HtmlInline);
388
429
  node->content = std::string(text, size);
389
430
  impl->nodeStack.top()->addChild(node);
390
431
  }
391
432
  break;
392
-
433
+
393
434
  case MD_TEXT_ENTITY:
394
435
  if (text && size > 0) {
395
- MD_OFFSET off = impl->lastTextEnd;
396
- ptrdiff_t diff = text - impl->inputText;
397
- if (diff >= 0 && static_cast<size_t>(diff) <= impl->inputTextSize) {
398
- off = static_cast<MD_OFFSET>(diff);
399
- }
436
+ MD_OFFSET off = safeOffset(text, impl->inputText, impl->inputTextSize);
437
+ if (off == 0 && text != impl->inputText) off = impl->lastTextEnd;
400
438
  if (impl->currentText.empty()) impl->currentTextBeg = off;
401
439
  impl->currentText.append(text, size);
402
440
  impl->lastTextEnd = off + size;
403
441
  }
404
442
  break;
405
-
443
+
406
444
  case MD_TEXT_NORMAL:
407
445
  case MD_TEXT_CODE:
408
446
  case MD_TEXT_LATEXMATH:
409
447
  default: {
410
448
  if (text && size > 0) {
411
- MD_OFFSET off = impl->lastTextEnd;
412
- ptrdiff_t diff = text - impl->inputText;
413
- if (diff >= 0 && static_cast<size_t>(diff) <= impl->inputTextSize) {
414
- off = static_cast<MD_OFFSET>(diff);
415
- }
416
-
449
+ MD_OFFSET off = safeOffset(text, impl->inputText, impl->inputTextSize);
450
+ if (off == 0 && text != impl->inputText) off = impl->lastTextEnd;
451
+
417
452
  if (impl->currentText.empty()) {
418
453
  impl->currentTextBeg = off;
419
454
  }
@@ -423,8 +458,11 @@ public:
423
458
  break;
424
459
  }
425
460
  }
426
-
461
+
427
462
  return 0;
463
+ } catch (...) {
464
+ return 1; // Signal error to md4c
465
+ }
428
466
  }
429
467
  };
430
468
 
@@ -463,10 +501,15 @@ std::shared_ptr<MarkdownNode> MD4CParser::parse(const std::string& markdown, con
463
501
  nullptr
464
502
  };
465
503
 
466
- md_parse(markdown.c_str(),
467
- static_cast<MD_SIZE>(inputSize),
468
- &parser,
469
- impl_.get());
504
+ int result = md_parse(markdown.c_str(),
505
+ static_cast<MD_SIZE>(inputSize),
506
+ &parser,
507
+ impl_.get());
508
+ if (result != 0) {
509
+ // md_parse failed (callback aborted or runtime error).
510
+ // The AST may be partial but is still a valid tree rooted at Document,
511
+ // so we continue rather than throw — callers can use whatever was parsed.
512
+ }
470
513
 
471
514
  impl_->flushText();
472
515
  return impl_->root;
@@ -0,0 +1,2 @@
1
+ // Implementation is provided by the platform-specific HybridObject (iOS: Swift, Android: Kotlin)
2
+ // via the Nitrogen-generated bridge. No C++ implementation is required here.
@@ -5,43 +5,50 @@ class HybridMarkdownSession: HybridMarkdownSessionSpec {
5
5
  private var buffer = ""
6
6
  private var listeners: [UUID: (Double, Double) -> Void] = [:]
7
7
  private let lock = NSLock()
8
-
9
- private(set) var version: Int = 0
10
-
11
- var highlightPosition: Double = 0
12
-
13
8
 
14
-
9
+ var highlightPosition: Double {
10
+ get { lock.lock(); defer { lock.unlock() }; return _highlightPosition }
11
+ set { lock.lock(); defer { lock.unlock() }; _highlightPosition = newValue }
12
+ }
13
+ private var _highlightPosition: Double = 0
14
+
15
15
  var memorySize: Int {
16
- return buffer.utf8.count + MemoryLayout<HybridMarkdownSession>.size
16
+ return buffer.utf8.count
17
+ + MemoryLayout<HybridMarkdownSession>.size
18
+ + MemoryLayout<NSLock>.size
19
+ + listeners.count * 128 // UUID key (16 bytes) + closure overhead estimate
17
20
  }
18
21
 
19
22
  private func utf16Length(_ value: String) -> Int {
20
23
  return (value as NSString).length
21
24
  }
22
-
25
+
23
26
  func append(chunk: String) throws -> Double {
24
- let from: Int
25
- let to: Int
26
- lock.lock()
27
- from = utf16Length(buffer)
28
- buffer += chunk
29
- to = utf16Length(buffer)
30
- version += 1
31
- lock.unlock()
32
- notifyListeners(from: Double(from), to: Double(to))
33
- return Double(to)
27
+ let notifyFrom: Double
28
+ let notifyTo: Double
29
+ do {
30
+ lock.lock()
31
+ defer { lock.unlock() }
32
+ let fromInt = utf16Length(buffer)
33
+ buffer += chunk
34
+ let toInt = utf16Length(buffer)
35
+ notifyFrom = Double(fromInt)
36
+ notifyTo = Double(toInt)
37
+ }
38
+ notifyListeners(from: notifyFrom, to: notifyTo)
39
+ return notifyTo
34
40
  }
35
-
41
+
36
42
  func clear() throws {
37
- lock.lock()
38
- buffer = ""
39
- highlightPosition = 0
40
- version += 1
41
- lock.unlock()
43
+ do {
44
+ lock.lock()
45
+ defer { lock.unlock() }
46
+ buffer = ""
47
+ _highlightPosition = 0
48
+ }
42
49
  notifyListeners(from: 0, to: 0)
43
50
  }
44
-
51
+
45
52
  func getAllText() throws -> String {
46
53
  lock.lock()
47
54
  defer { lock.unlock() }
@@ -55,6 +62,9 @@ class HybridMarkdownSession: HybridMarkdownSessionSpec {
55
62
  }
56
63
 
57
64
  func getTextRange(from: Double, to: Double) throws -> String {
65
+ guard from.isFinite && to.isFinite && from >= 0 && to >= 0 && from <= to else {
66
+ return ""
67
+ }
58
68
  lock.lock()
59
69
  defer { lock.unlock() }
60
70
 
@@ -64,43 +74,76 @@ class HybridMarkdownSession: HybridMarkdownSessionSpec {
64
74
  let end = max(start, min(Int(to), length))
65
75
  return text.substring(with: NSRange(location: start, length: end - start))
66
76
  }
67
-
77
+
78
+ /// Adds a change listener. The listener closure is held strongly.
79
+ /// To avoid retain cycles, ensure the listener does not capture a strong
80
+ /// reference to the object that owns this HybridMarkdownSession.
81
+ /// Always call the returned cleanup closure when done to unregister.
68
82
  func addListener(listener: @escaping (Double, Double) -> Void) throws -> () -> Void {
69
83
  let id = UUID()
70
84
  lock.lock()
71
85
  listeners[id] = listener
72
86
  lock.unlock()
73
-
87
+
74
88
  return { [weak self] in
75
- self?.lock.lock()
76
- self?.listeners.removeValue(forKey: id)
77
- self?.lock.unlock()
89
+ guard let self else { return }
90
+ self.lock.lock()
91
+ defer { self.lock.unlock() }
92
+ self.listeners.removeValue(forKey: id)
78
93
  }
79
94
  }
80
-
81
- func reset(text: String) -> Void {
82
- lock.lock()
83
- buffer = text
84
- lock.unlock()
85
- notifyListeners(from: 0, to: Double((text as NSString).length))
95
+
96
+ func reset(text: String) throws -> Void {
97
+ let notifyTo: Double
98
+ do {
99
+ lock.lock()
100
+ defer { lock.unlock() }
101
+ buffer = text
102
+ _highlightPosition = 0
103
+ notifyTo = Double((text as NSString).length)
104
+ }
105
+ notifyListeners(from: 0, to: notifyTo)
86
106
  }
87
107
 
88
- func replace(from: Double, to: Double, text: String) -> Double {
89
- lock.lock()
90
- let startIdx = buffer.index(buffer.startIndex, offsetBy: Int(from), limitedBy: buffer.endIndex) ?? buffer.endIndex
91
- let endIdx = buffer.index(buffer.startIndex, offsetBy: Int(to), limitedBy: buffer.endIndex) ?? buffer.endIndex
92
- buffer.replaceSubrange(startIdx..<endIdx, with: text)
93
- let newLength = Double((buffer as NSString).length)
94
- lock.unlock()
95
- notifyListeners(from: from, to: from + Double((text as NSString).length))
96
- return newLength
108
+ func replace(from: Double, to: Double, text: String) throws -> Double {
109
+ let notifyFrom: Double
110
+ let notifyTo: Double
111
+ let newLength: Double
112
+ let start: Int
113
+ let end: Int
114
+ do {
115
+ lock.lock()
116
+ defer { lock.unlock() }
117
+ guard from.isFinite && to.isFinite && from >= 0 && to >= 0 && from <= to else {
118
+ return Double(utf16Length(buffer))
119
+ }
120
+ let nsBuffer = NSMutableString(string: buffer)
121
+ let length = nsBuffer.length
122
+ start = max(0, min(Int(from), length))
123
+ end = max(start, min(Int(to), length))
124
+ nsBuffer.replaceCharacters(in: NSRange(location: start, length: end - start), with: text)
125
+ buffer = nsBuffer as String
126
+ newLength = Double((buffer as NSString).length)
127
+ notifyFrom = from
128
+ notifyTo = from + Double((text as NSString).length)
129
+ }
130
+ if start == end && text.isEmpty {
131
+ return newLength
132
+ }
133
+ notifyListeners(from: notifyFrom, to: notifyTo)
134
+ return newLength
97
135
  }
98
136
 
137
+ /// Notifies all registered listeners about a buffer change.
138
+ /// Called OUTSIDE the lock to prevent deadlock (listeners may call back into this session).
139
+ /// The `from`/`to` values reflect the buffer state AT THE TIME OF MUTATION.
140
+ /// The buffer may have been further modified by the time listener callbacks execute.
141
+ /// Listeners MUST NOT assume the buffer is stable; they should only use the index parameters.
99
142
  private func notifyListeners(from: Double, to: Double) {
100
143
  lock.lock()
101
144
  let currentListeners = Array(listeners.values)
102
145
  lock.unlock()
103
-
146
+
104
147
  for listener in currentListeners {
105
148
  listener(from, to)
106
149
  }