react-native-jet-markdown 0.2.0-beta.7

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 (157) hide show
  1. package/JetMarkdown.podspec +42 -0
  2. package/LICENSE +20 -0
  3. package/README.md +247 -0
  4. package/android/CMakeLists.txt +52 -0
  5. package/android/build.gradle +55 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/cpp/JniBindings.cpp +243 -0
  8. package/android/src/main/java/com/jetmarkdown/JetMarkdownNative.kt +114 -0
  9. package/android/src/main/java/com/jetmarkdown/JetMarkdownPackage.kt +22 -0
  10. package/android/src/main/java/com/jetmarkdown/JetMarkdownView.kt +235 -0
  11. package/android/src/main/java/com/jetmarkdown/JetMarkdownViewManager.kt +75 -0
  12. package/android/src/main/java/com/jetmarkdown/editor/EditorSpans.kt +217 -0
  13. package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorManager.kt +224 -0
  14. package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorView.kt +1736 -0
  15. package/android/src/main/java/com/jetmarkdown/measure/MarkdownMeasurer.kt +54 -0
  16. package/android/src/main/java/com/jetmarkdown/parser/AstDecoder.kt +59 -0
  17. package/android/src/main/java/com/jetmarkdown/parser/MdNode.kt +43 -0
  18. package/android/src/main/java/com/jetmarkdown/render/ContentCache.kt +53 -0
  19. package/android/src/main/java/com/jetmarkdown/render/RenderedContent.kt +318 -0
  20. package/android/src/main/java/com/jetmarkdown/render/SpannableRenderer.kt +658 -0
  21. package/android/src/main/java/com/jetmarkdown/render/spans/ChipSpan.kt +17 -0
  22. package/android/src/main/java/com/jetmarkdown/render/spans/InteractiveSpans.kt +11 -0
  23. package/android/src/main/java/com/jetmarkdown/render/spans/MarkdownLineHeightSpan.kt +59 -0
  24. package/android/src/main/java/com/jetmarkdown/render/spans/RunSpan.kt +40 -0
  25. package/android/src/main/java/com/jetmarkdown/style/Fonts.kt +34 -0
  26. package/android/src/main/java/com/jetmarkdown/style/LayoutStyleSpec.kt +71 -0
  27. package/android/src/main/java/com/jetmarkdown/style/PlatformColorResolver.kt +77 -0
  28. package/android/src/main/java/com/jetmarkdown/style/StyleConfig.kt +80 -0
  29. package/android/src/main/java/com/jetmarkdown/style/TextStyleSpec.kt +61 -0
  30. package/android/src/main/java/com/jetmarkdown/views/BlockStackView.kt +292 -0
  31. package/android/src/main/java/com/jetmarkdown/views/BlockTextView.kt +378 -0
  32. package/android/src/main/java/com/jetmarkdown/views/BoxDrawing.kt +50 -0
  33. package/android/src/main/java/com/jetmarkdown/views/MarkdownHost.kt +12 -0
  34. package/android/src/main/java/com/jetmarkdown/views/MarkdownImageView.kt +113 -0
  35. package/android/src/main/java/com/jetmarkdown/views/NestedHorizontalScrollView.kt +87 -0
  36. package/android/src/main/java/com/jetmarkdown/views/TableBlockView.kt +139 -0
  37. package/cpp/core/Ast.h +85 -0
  38. package/cpp/core/AstJson.cpp +129 -0
  39. package/cpp/core/AstJson.h +12 -0
  40. package/cpp/core/AstSerializer.cpp +40 -0
  41. package/cpp/core/AstSerializer.h +21 -0
  42. package/cpp/core/AstToMarkdown.cpp +549 -0
  43. package/cpp/core/AstToMarkdown.h +16 -0
  44. package/cpp/core/EditorRuns.cpp +640 -0
  45. package/cpp/core/EditorRuns.h +94 -0
  46. package/cpp/core/EditorText.cpp +15 -0
  47. package/cpp/core/EditorText.h +18 -0
  48. package/cpp/core/InlineExtensions.cpp +305 -0
  49. package/cpp/core/InlineExtensions.h +17 -0
  50. package/cpp/core/Parser.cpp +470 -0
  51. package/cpp/core/Parser.h +16 -0
  52. package/cpp/core/Preprocess.cpp +181 -0
  53. package/cpp/core/Preprocess.h +14 -0
  54. package/cpp/md4c/LICENSE +22 -0
  55. package/cpp/md4c/VERSION +1 -0
  56. package/cpp/md4c/md4c.c +6462 -0
  57. package/cpp/md4c/md4c.h +407 -0
  58. package/cpp/react/JetMarkdownEditorShadowNode.cpp +54 -0
  59. package/cpp/react/JetMarkdownEditorShadowNode.h +55 -0
  60. package/cpp/react/JetMarkdownEditorState.h +35 -0
  61. package/cpp/react/JetMarkdownMeasurer.cpp +32 -0
  62. package/cpp/react/JetMarkdownMeasurer.h +41 -0
  63. package/cpp/react/JetMarkdownShadowNode.cpp +100 -0
  64. package/cpp/react/JetMarkdownShadowNode.h +53 -0
  65. package/cpp/react/JetMarkdownState.h +52 -0
  66. package/cpp/react/override/react/renderer/components/JetMarkdownViewSpec/ComponentDescriptors.h +21 -0
  67. package/cpp/tests/parser_tests.cpp +662 -0
  68. package/cpp/tests/run_tests.sh +25 -0
  69. package/ios/JetMarkdownView.h +14 -0
  70. package/ios/JetMarkdownView.mm +468 -0
  71. package/ios/editor/JetMarkdownEditor.h +12 -0
  72. package/ios/editor/JetMarkdownEditor.mm +2002 -0
  73. package/ios/measure/JMDMarkdownMeasurer.h +22 -0
  74. package/ios/measure/JMDMarkdownMeasurer.mm +38 -0
  75. package/ios/render/JMDBlock.h +97 -0
  76. package/ios/render/JMDBlock.m +28 -0
  77. package/ios/render/JMDBlockRenderer.h +17 -0
  78. package/ios/render/JMDBlockRenderer.mm +852 -0
  79. package/ios/render/JMDContentCache.h +17 -0
  80. package/ios/render/JMDContentCache.mm +42 -0
  81. package/ios/render/JMDRenderedContent.h +35 -0
  82. package/ios/render/JMDRenderedContent.mm +274 -0
  83. package/ios/style/JMDFontScale.h +33 -0
  84. package/ios/style/JMDLayoutStyle.h +37 -0
  85. package/ios/style/JMDLayoutStyle.mm +68 -0
  86. package/ios/style/JMDStyleConfig.h +39 -0
  87. package/ios/style/JMDStyleConfig.mm +111 -0
  88. package/ios/style/JMDTextStyle.h +25 -0
  89. package/ios/style/JMDTextStyle.mm +68 -0
  90. package/ios/views/JMDBlockStackView.h +18 -0
  91. package/ios/views/JMDBlockStackView.m +97 -0
  92. package/ios/views/JMDBlockTextView.h +24 -0
  93. package/ios/views/JMDBlockTextView.m +340 -0
  94. package/ios/views/JMDBoxViews.h +33 -0
  95. package/ios/views/JMDBoxViews.m +292 -0
  96. package/ios/views/JMDImageView.h +19 -0
  97. package/ios/views/JMDImageView.m +68 -0
  98. package/ios/views/JMDMarkdownHost.h +16 -0
  99. package/ios/views/JMDTableView.h +16 -0
  100. package/ios/views/JMDTableView.m +172 -0
  101. package/lib/module/JetMarkdownEditor.js +85 -0
  102. package/lib/module/JetMarkdownEditor.js.map +1 -0
  103. package/lib/module/JetMarkdownEditor.native.js +244 -0
  104. package/lib/module/JetMarkdownEditor.native.js.map +1 -0
  105. package/lib/module/JetMarkdownEditorNativeComponent.ts +173 -0
  106. package/lib/module/JetMarkdownView.js +19 -0
  107. package/lib/module/JetMarkdownView.js.map +1 -0
  108. package/lib/module/JetMarkdownView.native.js +44 -0
  109. package/lib/module/JetMarkdownView.native.js.map +1 -0
  110. package/lib/module/JetMarkdownViewNativeComponent.ts +35 -0
  111. package/lib/module/defaultStyles.js +122 -0
  112. package/lib/module/defaultStyles.js.map +1 -0
  113. package/lib/module/index.js +9 -0
  114. package/lib/module/index.js.map +1 -0
  115. package/lib/module/package.json +1 -0
  116. package/lib/module/serializeStyles.js +259 -0
  117. package/lib/module/serializeStyles.js.map +1 -0
  118. package/lib/module/types.js +4 -0
  119. package/lib/module/types.js.map +1 -0
  120. package/lib/module/useJetMarkdownEditor.js +41 -0
  121. package/lib/module/useJetMarkdownEditor.js.map +1 -0
  122. package/lib/typescript/package.json +1 -0
  123. package/lib/typescript/src/JetMarkdownEditor.d.ts +6 -0
  124. package/lib/typescript/src/JetMarkdownEditor.d.ts.map +1 -0
  125. package/lib/typescript/src/JetMarkdownEditor.native.d.ts +6 -0
  126. package/lib/typescript/src/JetMarkdownEditor.native.d.ts.map +1 -0
  127. package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts +105 -0
  128. package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts.map +1 -0
  129. package/lib/typescript/src/JetMarkdownView.d.ts +3 -0
  130. package/lib/typescript/src/JetMarkdownView.d.ts.map +1 -0
  131. package/lib/typescript/src/JetMarkdownView.native.d.ts +3 -0
  132. package/lib/typescript/src/JetMarkdownView.native.d.ts.map +1 -0
  133. package/lib/typescript/src/JetMarkdownViewNativeComponent.d.ts +28 -0
  134. package/lib/typescript/src/JetMarkdownViewNativeComponent.d.ts.map +1 -0
  135. package/lib/typescript/src/defaultStyles.d.ts +20 -0
  136. package/lib/typescript/src/defaultStyles.d.ts.map +1 -0
  137. package/lib/typescript/src/index.d.ts +7 -0
  138. package/lib/typescript/src/index.d.ts.map +1 -0
  139. package/lib/typescript/src/serializeStyles.d.ts +34 -0
  140. package/lib/typescript/src/serializeStyles.d.ts.map +1 -0
  141. package/lib/typescript/src/types.d.ts +352 -0
  142. package/lib/typescript/src/types.d.ts.map +1 -0
  143. package/lib/typescript/src/useJetMarkdownEditor.d.ts +36 -0
  144. package/lib/typescript/src/useJetMarkdownEditor.d.ts.map +1 -0
  145. package/package.json +150 -0
  146. package/react-native.config.js +11 -0
  147. package/src/JetMarkdownEditor.native.tsx +301 -0
  148. package/src/JetMarkdownEditor.tsx +90 -0
  149. package/src/JetMarkdownEditorNativeComponent.ts +173 -0
  150. package/src/JetMarkdownView.native.tsx +67 -0
  151. package/src/JetMarkdownView.tsx +16 -0
  152. package/src/JetMarkdownViewNativeComponent.ts +35 -0
  153. package/src/defaultStyles.ts +102 -0
  154. package/src/index.tsx +34 -0
  155. package/src/serializeStyles.ts +369 -0
  156. package/src/types.ts +438 -0
  157. package/src/useJetMarkdownEditor.ts +50 -0
@@ -0,0 +1,640 @@
1
+ #include "EditorRuns.h"
2
+
3
+ #include <algorithm>
4
+ #include <set>
5
+
6
+ #include "Ast.h"
7
+ #include "AstToMarkdown.h"
8
+ #include "Parser.h"
9
+
10
+ namespace jetmarkdown {
11
+
12
+ namespace {
13
+
14
+ // Outer-to-inner nesting order when marks overlap. Inline code is innermost
15
+ // because markdown cannot format inside a code span; the serializer emits
16
+ // code content verbatim.
17
+ constexpr uint32_t kNestingOrder[] = {
18
+ MarkSpoiler,
19
+ MarkBold,
20
+ MarkItalic,
21
+ MarkStrikethrough,
22
+ MarkSuperscript,
23
+ MarkSubscript,
24
+ MarkInlineCode,
25
+ };
26
+
27
+ NodeType nodeTypeForMark(uint32_t mark) {
28
+ switch (mark) {
29
+ case MarkBold:
30
+ return NodeType::Bold;
31
+ case MarkItalic:
32
+ return NodeType::Italic;
33
+ case MarkStrikethrough:
34
+ return NodeType::Strikethrough;
35
+ case MarkSpoiler:
36
+ return NodeType::Spoiler;
37
+ case MarkSuperscript:
38
+ return NodeType::Superscript;
39
+ case MarkSubscript:
40
+ return NodeType::Subscript;
41
+ default:
42
+ return NodeType::InlineCode;
43
+ }
44
+ }
45
+
46
+ uint32_t markForNodeType(NodeType type) {
47
+ switch (type) {
48
+ case NodeType::Bold:
49
+ return MarkBold;
50
+ case NodeType::Italic:
51
+ return MarkItalic;
52
+ case NodeType::Strikethrough:
53
+ return MarkStrikethrough;
54
+ case NodeType::InlineCode:
55
+ return MarkInlineCode;
56
+ case NodeType::Spoiler:
57
+ return MarkSpoiler;
58
+ case NodeType::Superscript:
59
+ return MarkSuperscript;
60
+ case NodeType::Subscript:
61
+ return MarkSubscript;
62
+ default:
63
+ return 0;
64
+ }
65
+ }
66
+
67
+ // Maps every UTF-16 code-unit offset to a byte offset in the UTF-8 text.
68
+ // Index i holds the byte position where UTF-16 unit i starts; the final
69
+ // entry is text.size().
70
+ std::vector<size_t> utf16ToByteTable(const std::string& text) {
71
+ std::vector<size_t> table;
72
+ table.reserve(text.size() + 1);
73
+ size_t i = 0;
74
+ while (i < text.size()) {
75
+ const auto byte = static_cast<unsigned char>(text[i]);
76
+ size_t charLen = 1;
77
+ size_t utf16Len = 1;
78
+ if (byte >= 0xF0) {
79
+ charLen = 4;
80
+ utf16Len = 2; // surrogate pair
81
+ } else if (byte >= 0xE0) {
82
+ charLen = 3;
83
+ } else if (byte >= 0xC0) {
84
+ charLen = 2;
85
+ }
86
+ for (size_t u = 0; u < utf16Len; u++) {
87
+ table.push_back(i);
88
+ }
89
+ i += charLen;
90
+ }
91
+ table.push_back(text.size());
92
+ return table;
93
+ }
94
+
95
+ size_t utf16Length(const char* data, size_t size) {
96
+ size_t length = 0;
97
+ size_t i = 0;
98
+ while (i < size) {
99
+ const auto byte = static_cast<unsigned char>(data[i]);
100
+ if (byte >= 0xF0) {
101
+ length += 2;
102
+ i += 4;
103
+ } else if (byte >= 0xE0) {
104
+ length += 1;
105
+ i += 3;
106
+ } else if (byte >= 0xC0) {
107
+ length += 1;
108
+ i += 2;
109
+ } else {
110
+ length += 1;
111
+ i += 1;
112
+ }
113
+ }
114
+ return length;
115
+ }
116
+
117
+ struct Segment {
118
+ std::string text;
119
+ uint32_t flags = 0;
120
+ // Index into the links vector, or -1 when the piece is not linked.
121
+ int link = -1;
122
+ };
123
+
124
+ // Builds inline nodes for a line's segments. The mark whose run extends
125
+ // over the most upcoming segments wraps them (ties broken by
126
+ // kNestingOrder), which keeps overlapping runs nested instead of producing
127
+ // adjacent close/open delimiter runs that merge when re-parsed. Inline code
128
+ // never wraps other marks (markdown cannot format inside a code span), so
129
+ // it only groups exact-equal flag segments and stays innermost.
130
+ void buildInlineNodes(
131
+ MarkdownDocument& document,
132
+ Node* parent,
133
+ const std::vector<Segment>& segments,
134
+ size_t begin,
135
+ size_t end,
136
+ uint32_t handled) {
137
+ const auto extentOf = [&](size_t from, uint32_t mark) {
138
+ size_t j = from;
139
+ while (j < end && (segments[j].flags & ~handled & mark) != 0) {
140
+ j++;
141
+ }
142
+ return j - from;
143
+ };
144
+
145
+ size_t i = begin;
146
+ while (i < end) {
147
+ const uint32_t flags = segments[i].flags & ~handled;
148
+ if (flags == 0) {
149
+ Node* text = document.arena.alloc(NodeType::Text);
150
+ text->text = segments[i].text;
151
+ parent->children.push_back(text);
152
+ i++;
153
+ continue;
154
+ }
155
+ uint32_t mark = 0;
156
+ size_t extent = 0;
157
+ for (const uint32_t candidate : kNestingOrder) {
158
+ if (candidate == MarkInlineCode || (flags & candidate) == 0) {
159
+ continue;
160
+ }
161
+ const size_t candidateExtent = extentOf(i, candidate);
162
+ if (candidateExtent > extent) {
163
+ mark = candidate;
164
+ extent = candidateExtent;
165
+ }
166
+ }
167
+ if (mark == 0) {
168
+ Node* code = document.arena.alloc(NodeType::InlineCode);
169
+ size_t j = i;
170
+ while (j < end && (segments[j].flags & ~handled) == flags) {
171
+ code->text += segments[j].text;
172
+ j++;
173
+ }
174
+ parent->children.push_back(code);
175
+ i = j;
176
+ } else {
177
+ Node* wrapper = document.arena.alloc(nodeTypeForMark(mark));
178
+ parent->children.push_back(wrapper);
179
+ buildInlineNodes(document, wrapper, segments, i, i + extent, handled | mark);
180
+ i += extent;
181
+ }
182
+ }
183
+ }
184
+
185
+ struct EditorLineContent {
186
+ std::vector<Segment> segments;
187
+ EditorLine block;
188
+
189
+ bool empty() const {
190
+ return segments.empty();
191
+ }
192
+
193
+ std::string rawText() const {
194
+ std::string out;
195
+ for (const Segment& segment : segments) {
196
+ out += segment.text;
197
+ }
198
+ return out;
199
+ }
200
+ };
201
+
202
+ // Builds a line's inline nodes: consecutive same-link segments wrap in a
203
+ // Link node (links are outermost — markdown formats inside the label).
204
+ void buildLineNodes(
205
+ MarkdownDocument& document,
206
+ Node* parent,
207
+ const std::vector<Segment>& segments,
208
+ const std::vector<LinkRun>& links) {
209
+ size_t i = 0;
210
+ while (i < segments.size()) {
211
+ const int link = segments[i].link;
212
+ size_t j = i;
213
+ while (j < segments.size() && segments[j].link == link) {
214
+ j++;
215
+ }
216
+ if (link >= 0 && static_cast<size_t>(link) < links.size()) {
217
+ Node* node = document.arena.alloc(NodeType::Link);
218
+ node->url = links[static_cast<size_t>(link)].url;
219
+ parent->children.push_back(node);
220
+ buildInlineNodes(document, node, segments, i, j, 0);
221
+ } else {
222
+ buildInlineNodes(document, parent, segments, i, j, 0);
223
+ }
224
+ i = j;
225
+ }
226
+ }
227
+
228
+ // ---------------------------------------------------------------------------
229
+ // Markdown extraction (parse -> editor document)
230
+ // ---------------------------------------------------------------------------
231
+
232
+ void appendMarkedText(EditorDocument& out, const std::string& text, uint32_t flags) {
233
+ if (text.empty()) {
234
+ return;
235
+ }
236
+ const uint32_t start = out.textUtf16Length;
237
+ out.text += text;
238
+ out.textUtf16Length +=
239
+ static_cast<uint32_t>(utf16Length(text.data(), text.size()));
240
+ if (flags != 0) {
241
+ const uint32_t end = out.textUtf16Length;
242
+ if (!out.runs.empty() && out.runs.back().flags == flags &&
243
+ out.runs.back().end == start) {
244
+ out.runs.back().end = end;
245
+ } else {
246
+ out.runs.push_back({start, end, flags});
247
+ }
248
+ }
249
+ }
250
+
251
+ struct EditorCollector {
252
+ EditorDocument& out;
253
+ bool atBlockStart = true;
254
+ EditorLine currentLine;
255
+
256
+ // Completes the current text line: every '\n' appended to out.text gets a
257
+ // matching entry in out.lines.
258
+ void endLine() {
259
+ out.lines.push_back(currentLine);
260
+ out.text += '\n';
261
+ out.textUtf16Length += 1;
262
+ }
263
+
264
+ void beginBlock(const EditorLine& line) {
265
+ if (!atBlockStart && !out.text.empty()) {
266
+ endLine();
267
+ }
268
+ currentLine = line;
269
+ atBlockStart = true;
270
+ }
271
+
272
+ void collectChildren(const Node* node, uint32_t activeFlags, EditorLine context) {
273
+ for (const Node* child : node->children) {
274
+ collect(child, activeFlags, context);
275
+ }
276
+ }
277
+
278
+ void collect(const Node* node, uint32_t activeFlags, EditorLine context) {
279
+ const uint32_t mark = markForNodeType(node->type);
280
+ if (mark != 0 && node->type != NodeType::InlineCode) {
281
+ collectChildren(node, activeFlags | mark, context);
282
+ return;
283
+ }
284
+ switch (node->type) {
285
+ case NodeType::Text:
286
+ appendMarkedText(out, node->text, activeFlags);
287
+ break;
288
+ case NodeType::SoftBreak:
289
+ case NodeType::HardBreak:
290
+ endLine();
291
+ currentLine = context;
292
+ break;
293
+ case NodeType::InlineCode:
294
+ appendMarkedText(out, node->text, activeFlags | MarkInlineCode);
295
+ break;
296
+ case NodeType::Link: {
297
+ const uint32_t start = out.textUtf16Length;
298
+ collectChildren(node, activeFlags, context);
299
+ const uint32_t end = out.textUtf16Length;
300
+ if (end > start) {
301
+ out.links.push_back({start, end, node->url});
302
+ }
303
+ break;
304
+ }
305
+ case NodeType::Image:
306
+ appendMarkedText(out, node->text, activeFlags);
307
+ break;
308
+ case NodeType::CodeBlock: {
309
+ beginBlock({EditorBlockType::Code, 0});
310
+ std::string content = node->text;
311
+ if (!content.empty() && content.back() == '\n') {
312
+ content.pop_back();
313
+ }
314
+ size_t lineStart = 0;
315
+ while (true) {
316
+ const size_t newline = content.find('\n', lineStart);
317
+ const std::string piece = content.substr(
318
+ lineStart, newline == std::string::npos ? std::string::npos
319
+ : newline - lineStart);
320
+ out.text += piece;
321
+ out.textUtf16Length +=
322
+ static_cast<uint32_t>(utf16Length(piece.data(), piece.size()));
323
+ if (newline == std::string::npos) {
324
+ break;
325
+ }
326
+ endLine();
327
+ currentLine = {EditorBlockType::Code, 0};
328
+ lineStart = newline + 1;
329
+ }
330
+ atBlockStart = false;
331
+ break;
332
+ }
333
+ case NodeType::Heading:
334
+ beginBlock({EditorBlockType::Heading, node->level});
335
+ collectChildren(node, activeFlags, {EditorBlockType::Heading, node->level});
336
+ atBlockStart = false;
337
+ break;
338
+ case NodeType::Paragraph:
339
+ case NodeType::TableRow:
340
+ beginBlock(context);
341
+ collectChildren(node, activeFlags, context);
342
+ atBlockStart = false;
343
+ break;
344
+ case NodeType::BlockQuote:
345
+ // Innermost block wins: lists inside a quote become list lines.
346
+ collectChildren(node, activeFlags, {EditorBlockType::Quote, 0});
347
+ break;
348
+ case NodeType::List: {
349
+ const EditorLine itemLine = {
350
+ node->ordered ? EditorBlockType::Ordered : EditorBlockType::Bullet, 0};
351
+ for (const Node* item : node->children) {
352
+ // Tight items hold inline children directly; force a fresh line
353
+ // for each item, then collect content in item context.
354
+ beginBlock(itemLine);
355
+ collectChildren(item, activeFlags, itemLine);
356
+ atBlockStart = false;
357
+ }
358
+ break;
359
+ }
360
+ case NodeType::TableCell:
361
+ if (!atBlockStart && !out.text.empty() && out.text.back() != '\n') {
362
+ out.text += ' ';
363
+ out.textUtf16Length += 1;
364
+ }
365
+ collectChildren(node, activeFlags, context);
366
+ break;
367
+ case NodeType::ThematicBreak:
368
+ break;
369
+ default:
370
+ collectChildren(node, activeFlags, context);
371
+ break;
372
+ }
373
+ }
374
+
375
+ void finish() {
376
+ // Line entries exist per newline; the final (unterminated) line gets
377
+ // its entry here so lines.size() == line count.
378
+ out.lines.push_back(currentLine);
379
+ }
380
+ };
381
+
382
+ } // namespace
383
+
384
+ std::string markdownFromEditor(
385
+ const std::string& text,
386
+ const std::vector<StyledRun>& runs,
387
+ const std::vector<EditorLine>& lines,
388
+ const std::vector<LinkRun>& links) {
389
+ const std::vector<size_t> toByte = utf16ToByteTable(text);
390
+ const auto utf16Size = static_cast<uint32_t>(toByte.size() - 1);
391
+
392
+ // Emphasis delimiters cannot open before or close after whitespace
393
+ // (flanking rules), so runs are trimmed inward past edge spaces — the
394
+ // spaces fall outside as plain text. Code-only runs keep their spaces:
395
+ // the code-span serializer pads them instead.
396
+ const auto isEdgeSpace = [&](uint32_t offset) {
397
+ const char c = text[toByte[offset]];
398
+ return c == ' ' || c == '\t';
399
+ };
400
+ std::vector<StyledRun> trimmed;
401
+ trimmed.reserve(runs.size());
402
+ for (StyledRun run : runs) {
403
+ run.start = std::min(run.start, utf16Size);
404
+ run.end = std::min(run.end, utf16Size);
405
+ if ((run.flags & ~MarkInlineCode) != 0) {
406
+ while (run.start < run.end && isEdgeSpace(run.start)) {
407
+ run.start++;
408
+ }
409
+ while (run.end > run.start && isEdgeSpace(run.end - 1)) {
410
+ run.end--;
411
+ }
412
+ }
413
+ if (run.start < run.end) {
414
+ trimmed.push_back(run);
415
+ }
416
+ }
417
+
418
+ // Split at every run/link edge and newline so each piece has constant
419
+ // flags and link membership.
420
+ std::set<uint32_t> cuts = {0, utf16Size};
421
+ for (const StyledRun& run : trimmed) {
422
+ cuts.insert(run.start);
423
+ cuts.insert(run.end);
424
+ }
425
+ for (const LinkRun& link : links) {
426
+ cuts.insert(std::min(link.start, utf16Size));
427
+ cuts.insert(std::min(link.end, utf16Size));
428
+ }
429
+ for (uint32_t u = 0; u < utf16Size; u++) {
430
+ if (text[toByte[u]] == '\n') {
431
+ cuts.insert(u);
432
+ cuts.insert(u + 1);
433
+ }
434
+ }
435
+
436
+ // Collect lines (keeping empties so indices align with `lines`).
437
+ std::vector<EditorLineContent> collected;
438
+ collected.push_back({});
439
+ size_t lineIndex = 0;
440
+ const auto blockForLine = [&](size_t index) -> EditorLine {
441
+ return index < lines.size() ? lines[index] : EditorLine{};
442
+ };
443
+ collected.back().block = blockForLine(0);
444
+
445
+ // Sweep instead of rescanning every run per piece (which made heavily
446
+ // styled documents O(pieces × runs)). Run edges are all cuts, so a run
447
+ // covers piece [prev, next) exactly when start <= prev < end.
448
+ std::vector<std::pair<uint32_t, uint32_t>> runStarts;
449
+ std::vector<std::pair<uint32_t, uint32_t>> runEnds;
450
+ runStarts.reserve(trimmed.size());
451
+ runEnds.reserve(trimmed.size());
452
+ for (const StyledRun& run : trimmed) {
453
+ runStarts.push_back({run.start, run.flags});
454
+ runEnds.push_back({run.end, run.flags});
455
+ }
456
+ std::sort(runStarts.begin(), runStarts.end());
457
+ std::sort(runEnds.begin(), runEnds.end());
458
+ std::vector<size_t> linkOrder(links.size());
459
+ for (size_t l = 0; l < links.size(); l++) {
460
+ linkOrder[l] = l;
461
+ }
462
+ std::sort(linkOrder.begin(), linkOrder.end(), [&](size_t a, size_t b) {
463
+ return links[a].start < links[b].start;
464
+ });
465
+
466
+ int bitCounts[32] = {0};
467
+ const auto adjustFlags = [&](uint32_t flags, int delta) {
468
+ for (int bit = 0; bit < 32; bit++) {
469
+ if (flags & (1u << bit)) {
470
+ bitCounts[bit] += delta;
471
+ }
472
+ }
473
+ };
474
+ const auto activeFlags = [&]() {
475
+ uint32_t flags = 0;
476
+ for (int bit = 0; bit < 32; bit++) {
477
+ if (bitCounts[bit] > 0) {
478
+ flags |= 1u << bit;
479
+ }
480
+ }
481
+ return flags;
482
+ };
483
+
484
+ size_t startIdx = 0;
485
+ size_t endIdx = 0;
486
+ size_t linkPtr = 0;
487
+
488
+ auto it = cuts.begin();
489
+ uint32_t prev = *it;
490
+ for (++it; it != cuts.end(); ++it) {
491
+ const uint32_t next = *it;
492
+ while (endIdx < runEnds.size() && runEnds[endIdx].first <= prev) {
493
+ adjustFlags(runEnds[endIdx].second, -1);
494
+ endIdx++;
495
+ }
496
+ while (startIdx < runStarts.size() && runStarts[startIdx].first <= prev) {
497
+ adjustFlags(runStarts[startIdx].second, 1);
498
+ startIdx++;
499
+ }
500
+ while (linkPtr < linkOrder.size() &&
501
+ links[linkOrder[linkPtr]].end <= prev) {
502
+ linkPtr++;
503
+ }
504
+
505
+ const std::string piece =
506
+ text.substr(toByte[prev], toByte[next] - toByte[prev]);
507
+ if (piece == "\n") {
508
+ lineIndex++;
509
+ collected.push_back({});
510
+ collected.back().block = blockForLine(lineIndex);
511
+ } else if (!piece.empty()) {
512
+ int linkIndex = -1;
513
+ if (linkPtr < linkOrder.size() &&
514
+ links[linkOrder[linkPtr]].start <= prev &&
515
+ next <= links[linkOrder[linkPtr]].end) {
516
+ linkIndex = static_cast<int>(linkOrder[linkPtr]);
517
+ }
518
+ collected.back().segments.push_back({piece, activeFlags(), linkIndex});
519
+ }
520
+ prev = next;
521
+ }
522
+
523
+ // Group consecutive same-block lines into markdown block nodes.
524
+ MarkdownDocument document;
525
+ Node* root = document.arena.alloc(NodeType::Document);
526
+ document.root = root;
527
+
528
+ size_t i = 0;
529
+ while (i < collected.size()) {
530
+ const EditorLineContent& line = collected[i];
531
+ switch (line.block.type) {
532
+ case EditorBlockType::Heading: {
533
+ if (!line.empty()) {
534
+ Node* heading = document.arena.alloc(NodeType::Heading);
535
+ heading->level = std::clamp<uint8_t>(line.block.level, 1, 6);
536
+ root->children.push_back(heading);
537
+ buildLineNodes(document, heading, line.segments, links);
538
+ }
539
+ i++;
540
+ break;
541
+ }
542
+ case EditorBlockType::Quote: {
543
+ Node* quote = document.arena.alloc(NodeType::BlockQuote);
544
+ bool any = false;
545
+ while (i < collected.size() &&
546
+ collected[i].block.type == EditorBlockType::Quote) {
547
+ if (!collected[i].empty()) {
548
+ Node* paragraph = document.arena.alloc(NodeType::Paragraph);
549
+ quote->children.push_back(paragraph);
550
+ buildLineNodes(document, paragraph, collected[i].segments, links);
551
+ any = true;
552
+ }
553
+ i++;
554
+ }
555
+ if (any) {
556
+ root->children.push_back(quote);
557
+ }
558
+ break;
559
+ }
560
+ case EditorBlockType::Code: {
561
+ // Raw text; inline marks cannot exist inside a code fence. Empty
562
+ // interior lines stay; the fence itself provides the boundaries.
563
+ Node* code = document.arena.alloc(NodeType::CodeBlock);
564
+ std::string content;
565
+ bool any = false;
566
+ while (i < collected.size() &&
567
+ collected[i].block.type == EditorBlockType::Code) {
568
+ if (!content.empty() || any) {
569
+ content += '\n';
570
+ }
571
+ content += collected[i].rawText();
572
+ any = true;
573
+ i++;
574
+ }
575
+ if (any && !content.empty()) {
576
+ code->text = content + "\n";
577
+ root->children.push_back(code);
578
+ }
579
+ break;
580
+ }
581
+ case EditorBlockType::Bullet:
582
+ case EditorBlockType::Ordered: {
583
+ const EditorBlockType type = line.block.type;
584
+ Node* list = document.arena.alloc(NodeType::List);
585
+ list->ordered = type == EditorBlockType::Ordered;
586
+ list->startIndex = 1;
587
+ bool any = false;
588
+ while (i < collected.size() && collected[i].block.type == type) {
589
+ if (!collected[i].empty()) {
590
+ Node* item = document.arena.alloc(NodeType::ListItem);
591
+ list->children.push_back(item);
592
+ buildLineNodes(document, item, collected[i].segments, links);
593
+ any = true;
594
+ }
595
+ i++;
596
+ }
597
+ if (any) {
598
+ root->children.push_back(list);
599
+ }
600
+ break;
601
+ }
602
+ case EditorBlockType::Paragraph:
603
+ default: {
604
+ if (!line.empty()) {
605
+ Node* paragraph = document.arena.alloc(NodeType::Paragraph);
606
+ root->children.push_back(paragraph);
607
+ buildLineNodes(document, paragraph, line.segments, links);
608
+ }
609
+ i++;
610
+ break;
611
+ }
612
+ }
613
+ }
614
+
615
+ return astToMarkdown(root);
616
+ }
617
+
618
+ EditorDocument editorFromMarkdown(const std::string& markdown) {
619
+ auto document = parseMarkdown(markdown);
620
+ EditorDocument out;
621
+ EditorCollector collector{out, true, EditorLine{}};
622
+ if (document->root != nullptr) {
623
+ collector.collectChildren(document->root, 0, EditorLine{});
624
+ }
625
+ collector.finish();
626
+ return out;
627
+ }
628
+
629
+ std::string markdownFromStyledText(
630
+ const std::string& text,
631
+ const std::vector<StyledRun>& runs) {
632
+ return markdownFromEditor(text, runs, {});
633
+ }
634
+
635
+ StyledText styledTextFromMarkdown(const std::string& markdown) {
636
+ EditorDocument document = editorFromMarkdown(markdown);
637
+ return {std::move(document.text), std::move(document.runs)};
638
+ }
639
+
640
+ } // namespace jetmarkdown