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.
- package/README.md +257 -682
- package/android/CMakeLists.txt +8 -1
- package/android/build.gradle +9 -2
- package/android/consumer-rules.pro +31 -0
- package/android/gradle.properties +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +4 -1
- package/android/src/main/java/com/margelo/nitro/com/nitromarkdown/HybridMarkdownSession.kt +61 -21
- package/android/src/main/java/com/nitromarkdown/NitroMarkdownPackage.kt +6 -18
- package/cpp/bindings/HybridMarkdownParser.cpp +38 -12
- package/cpp/bindings/HybridMarkdownParser.hpp +4 -4
- package/cpp/bindings/HybridMarkdownSession.cpp +2 -0
- package/cpp/core/MD4CParser.cpp +128 -85
- package/cpp/core/MarkdownSessionCore.cpp +2 -0
- package/ios/HybridMarkdownSession.swift +89 -46
- package/lib/commonjs/headless.js +33 -7
- package/lib/commonjs/headless.js.map +1 -1
- package/lib/commonjs/index.js +48 -38
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/markdown-stream.js +1 -1
- package/lib/commonjs/markdown-stream.js.map +1 -1
- package/lib/commonjs/markdown.js +47 -10
- package/lib/commonjs/markdown.js.map +1 -1
- package/lib/commonjs/renderers/code.js +1 -1
- package/lib/commonjs/renderers/code.js.map +1 -1
- package/lib/commonjs/renderers/image.js +6 -1
- package/lib/commonjs/renderers/image.js.map +1 -1
- package/lib/commonjs/renderers/link.js +7 -2
- package/lib/commonjs/renderers/link.js.map +1 -1
- package/lib/commonjs/renderers/list.js +2 -0
- package/lib/commonjs/renderers/list.js.map +1 -1
- package/lib/commonjs/renderers/math.js +4 -2
- package/lib/commonjs/renderers/math.js.map +1 -1
- package/lib/commonjs/renderers/table/cell-content.js +1 -1
- package/lib/commonjs/renderers/table/cell-content.js.map +1 -1
- package/lib/commonjs/renderers/table/index.js +10 -2
- package/lib/commonjs/renderers/table/index.js.map +1 -1
- package/lib/commonjs/theme.js +7 -7
- package/lib/commonjs/theme.js.map +1 -1
- package/lib/commonjs/utils/code-highlight.js +24 -25
- package/lib/commonjs/utils/code-highlight.js.map +1 -1
- package/lib/module/headless.js +34 -6
- package/lib/module/headless.js.map +1 -1
- package/lib/module/index.js +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/markdown-stream.js +1 -1
- package/lib/module/markdown-stream.js.map +1 -1
- package/lib/module/markdown.js +48 -11
- package/lib/module/markdown.js.map +1 -1
- package/lib/module/renderers/code.js +1 -1
- package/lib/module/renderers/code.js.map +1 -1
- package/lib/module/renderers/image.js +6 -1
- package/lib/module/renderers/image.js.map +1 -1
- package/lib/module/renderers/link.js +7 -2
- package/lib/module/renderers/link.js.map +1 -1
- package/lib/module/renderers/list.js +2 -0
- package/lib/module/renderers/list.js.map +1 -1
- package/lib/module/renderers/math.js +4 -2
- package/lib/module/renderers/math.js.map +1 -1
- package/lib/module/renderers/table/cell-content.js +1 -1
- package/lib/module/renderers/table/cell-content.js.map +1 -1
- package/lib/module/renderers/table/index.js +10 -2
- package/lib/module/renderers/table/index.js.map +1 -1
- package/lib/module/theme.js +7 -7
- package/lib/module/theme.js.map +1 -1
- package/lib/module/utils/code-highlight.js +24 -25
- package/lib/module/utils/code-highlight.js.map +1 -1
- package/lib/typescript/commonjs/headless.d.ts +9 -1
- package/lib/typescript/commonjs/headless.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +3 -2
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/commonjs/markdown.d.ts +7 -2
- package/lib/typescript/commonjs/markdown.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/code.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/image.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/link.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/list.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/math.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/table/cell-content.d.ts +4 -3
- package/lib/typescript/commonjs/renderers/table/cell-content.d.ts.map +1 -1
- package/lib/typescript/commonjs/renderers/table/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/theme.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/code-highlight.d.ts +1 -1
- package/lib/typescript/commonjs/utils/code-highlight.d.ts.map +1 -1
- package/lib/typescript/module/headless.d.ts +9 -1
- package/lib/typescript/module/headless.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +3 -2
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/markdown-stream.d.ts.map +1 -1
- package/lib/typescript/module/markdown.d.ts +7 -2
- package/lib/typescript/module/markdown.d.ts.map +1 -1
- package/lib/typescript/module/renderers/code.d.ts.map +1 -1
- package/lib/typescript/module/renderers/image.d.ts.map +1 -1
- package/lib/typescript/module/renderers/link.d.ts.map +1 -1
- package/lib/typescript/module/renderers/list.d.ts.map +1 -1
- package/lib/typescript/module/renderers/math.d.ts.map +1 -1
- package/lib/typescript/module/renderers/table/cell-content.d.ts +4 -3
- package/lib/typescript/module/renderers/table/cell-content.d.ts.map +1 -1
- package/lib/typescript/module/renderers/table/index.d.ts.map +1 -1
- package/lib/typescript/module/theme.d.ts.map +1 -1
- package/lib/typescript/module/utils/code-highlight.d.ts +1 -1
- package/lib/typescript/module/utils/code-highlight.d.ts.map +1 -1
- package/package.json +5 -3
- package/src/headless.ts +57 -7
- package/src/index.ts +16 -2
- package/src/markdown-stream.tsx +1 -0
- package/src/markdown.tsx +98 -31
- package/src/renderers/code.tsx +23 -16
- package/src/renderers/image.tsx +9 -1
- package/src/renderers/link.tsx +8 -2
- package/src/renderers/list.tsx +2 -0
- package/src/renderers/math.tsx +6 -2
- package/src/renderers/table/cell-content.tsx +15 -4
- package/src/renderers/table/index.tsx +15 -3
- package/src/theme.ts +34 -14
- package/src/utils/code-highlight.ts +133 -44
package/cpp/core/MD4CParser.cpp
CHANGED
|
@@ -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()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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->
|
|
362
|
-
|
|
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.
|
|
375
|
-
|
|
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.
|
|
381
|
-
|
|
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->
|
|
396
|
-
|
|
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->
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
468
|
-
|
|
469
|
-
|
|
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;
|
|
@@ -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
|
|
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
|
|
25
|
-
let
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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
|
|
76
|
-
self
|
|
77
|
-
self
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
}
|