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.
- package/JetMarkdown.podspec +42 -0
- package/LICENSE +20 -0
- package/README.md +247 -0
- package/android/CMakeLists.txt +52 -0
- package/android/build.gradle +55 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/JniBindings.cpp +243 -0
- package/android/src/main/java/com/jetmarkdown/JetMarkdownNative.kt +114 -0
- package/android/src/main/java/com/jetmarkdown/JetMarkdownPackage.kt +22 -0
- package/android/src/main/java/com/jetmarkdown/JetMarkdownView.kt +235 -0
- package/android/src/main/java/com/jetmarkdown/JetMarkdownViewManager.kt +75 -0
- package/android/src/main/java/com/jetmarkdown/editor/EditorSpans.kt +217 -0
- package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorManager.kt +224 -0
- package/android/src/main/java/com/jetmarkdown/editor/JetMarkdownEditorView.kt +1736 -0
- package/android/src/main/java/com/jetmarkdown/measure/MarkdownMeasurer.kt +54 -0
- package/android/src/main/java/com/jetmarkdown/parser/AstDecoder.kt +59 -0
- package/android/src/main/java/com/jetmarkdown/parser/MdNode.kt +43 -0
- package/android/src/main/java/com/jetmarkdown/render/ContentCache.kt +53 -0
- package/android/src/main/java/com/jetmarkdown/render/RenderedContent.kt +318 -0
- package/android/src/main/java/com/jetmarkdown/render/SpannableRenderer.kt +658 -0
- package/android/src/main/java/com/jetmarkdown/render/spans/ChipSpan.kt +17 -0
- package/android/src/main/java/com/jetmarkdown/render/spans/InteractiveSpans.kt +11 -0
- package/android/src/main/java/com/jetmarkdown/render/spans/MarkdownLineHeightSpan.kt +59 -0
- package/android/src/main/java/com/jetmarkdown/render/spans/RunSpan.kt +40 -0
- package/android/src/main/java/com/jetmarkdown/style/Fonts.kt +34 -0
- package/android/src/main/java/com/jetmarkdown/style/LayoutStyleSpec.kt +71 -0
- package/android/src/main/java/com/jetmarkdown/style/PlatformColorResolver.kt +77 -0
- package/android/src/main/java/com/jetmarkdown/style/StyleConfig.kt +80 -0
- package/android/src/main/java/com/jetmarkdown/style/TextStyleSpec.kt +61 -0
- package/android/src/main/java/com/jetmarkdown/views/BlockStackView.kt +292 -0
- package/android/src/main/java/com/jetmarkdown/views/BlockTextView.kt +378 -0
- package/android/src/main/java/com/jetmarkdown/views/BoxDrawing.kt +50 -0
- package/android/src/main/java/com/jetmarkdown/views/MarkdownHost.kt +12 -0
- package/android/src/main/java/com/jetmarkdown/views/MarkdownImageView.kt +113 -0
- package/android/src/main/java/com/jetmarkdown/views/NestedHorizontalScrollView.kt +87 -0
- package/android/src/main/java/com/jetmarkdown/views/TableBlockView.kt +139 -0
- package/cpp/core/Ast.h +85 -0
- package/cpp/core/AstJson.cpp +129 -0
- package/cpp/core/AstJson.h +12 -0
- package/cpp/core/AstSerializer.cpp +40 -0
- package/cpp/core/AstSerializer.h +21 -0
- package/cpp/core/AstToMarkdown.cpp +549 -0
- package/cpp/core/AstToMarkdown.h +16 -0
- package/cpp/core/EditorRuns.cpp +640 -0
- package/cpp/core/EditorRuns.h +94 -0
- package/cpp/core/EditorText.cpp +15 -0
- package/cpp/core/EditorText.h +18 -0
- package/cpp/core/InlineExtensions.cpp +305 -0
- package/cpp/core/InlineExtensions.h +17 -0
- package/cpp/core/Parser.cpp +470 -0
- package/cpp/core/Parser.h +16 -0
- package/cpp/core/Preprocess.cpp +181 -0
- package/cpp/core/Preprocess.h +14 -0
- package/cpp/md4c/LICENSE +22 -0
- package/cpp/md4c/VERSION +1 -0
- package/cpp/md4c/md4c.c +6462 -0
- package/cpp/md4c/md4c.h +407 -0
- package/cpp/react/JetMarkdownEditorShadowNode.cpp +54 -0
- package/cpp/react/JetMarkdownEditorShadowNode.h +55 -0
- package/cpp/react/JetMarkdownEditorState.h +35 -0
- package/cpp/react/JetMarkdownMeasurer.cpp +32 -0
- package/cpp/react/JetMarkdownMeasurer.h +41 -0
- package/cpp/react/JetMarkdownShadowNode.cpp +100 -0
- package/cpp/react/JetMarkdownShadowNode.h +53 -0
- package/cpp/react/JetMarkdownState.h +52 -0
- package/cpp/react/override/react/renderer/components/JetMarkdownViewSpec/ComponentDescriptors.h +21 -0
- package/cpp/tests/parser_tests.cpp +662 -0
- package/cpp/tests/run_tests.sh +25 -0
- package/ios/JetMarkdownView.h +14 -0
- package/ios/JetMarkdownView.mm +468 -0
- package/ios/editor/JetMarkdownEditor.h +12 -0
- package/ios/editor/JetMarkdownEditor.mm +2002 -0
- package/ios/measure/JMDMarkdownMeasurer.h +22 -0
- package/ios/measure/JMDMarkdownMeasurer.mm +38 -0
- package/ios/render/JMDBlock.h +97 -0
- package/ios/render/JMDBlock.m +28 -0
- package/ios/render/JMDBlockRenderer.h +17 -0
- package/ios/render/JMDBlockRenderer.mm +852 -0
- package/ios/render/JMDContentCache.h +17 -0
- package/ios/render/JMDContentCache.mm +42 -0
- package/ios/render/JMDRenderedContent.h +35 -0
- package/ios/render/JMDRenderedContent.mm +274 -0
- package/ios/style/JMDFontScale.h +33 -0
- package/ios/style/JMDLayoutStyle.h +37 -0
- package/ios/style/JMDLayoutStyle.mm +68 -0
- package/ios/style/JMDStyleConfig.h +39 -0
- package/ios/style/JMDStyleConfig.mm +111 -0
- package/ios/style/JMDTextStyle.h +25 -0
- package/ios/style/JMDTextStyle.mm +68 -0
- package/ios/views/JMDBlockStackView.h +18 -0
- package/ios/views/JMDBlockStackView.m +97 -0
- package/ios/views/JMDBlockTextView.h +24 -0
- package/ios/views/JMDBlockTextView.m +340 -0
- package/ios/views/JMDBoxViews.h +33 -0
- package/ios/views/JMDBoxViews.m +292 -0
- package/ios/views/JMDImageView.h +19 -0
- package/ios/views/JMDImageView.m +68 -0
- package/ios/views/JMDMarkdownHost.h +16 -0
- package/ios/views/JMDTableView.h +16 -0
- package/ios/views/JMDTableView.m +172 -0
- package/lib/module/JetMarkdownEditor.js +85 -0
- package/lib/module/JetMarkdownEditor.js.map +1 -0
- package/lib/module/JetMarkdownEditor.native.js +244 -0
- package/lib/module/JetMarkdownEditor.native.js.map +1 -0
- package/lib/module/JetMarkdownEditorNativeComponent.ts +173 -0
- package/lib/module/JetMarkdownView.js +19 -0
- package/lib/module/JetMarkdownView.js.map +1 -0
- package/lib/module/JetMarkdownView.native.js +44 -0
- package/lib/module/JetMarkdownView.native.js.map +1 -0
- package/lib/module/JetMarkdownViewNativeComponent.ts +35 -0
- package/lib/module/defaultStyles.js +122 -0
- package/lib/module/defaultStyles.js.map +1 -0
- package/lib/module/index.js +9 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/serializeStyles.js +259 -0
- package/lib/module/serializeStyles.js.map +1 -0
- package/lib/module/types.js +4 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useJetMarkdownEditor.js +41 -0
- package/lib/module/useJetMarkdownEditor.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/JetMarkdownEditor.d.ts +6 -0
- package/lib/typescript/src/JetMarkdownEditor.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownEditor.native.d.ts +6 -0
- package/lib/typescript/src/JetMarkdownEditor.native.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts +105 -0
- package/lib/typescript/src/JetMarkdownEditorNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownView.d.ts +3 -0
- package/lib/typescript/src/JetMarkdownView.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownView.native.d.ts +3 -0
- package/lib/typescript/src/JetMarkdownView.native.d.ts.map +1 -0
- package/lib/typescript/src/JetMarkdownViewNativeComponent.d.ts +28 -0
- package/lib/typescript/src/JetMarkdownViewNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/defaultStyles.d.ts +20 -0
- package/lib/typescript/src/defaultStyles.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +7 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/serializeStyles.d.ts +34 -0
- package/lib/typescript/src/serializeStyles.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +352 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/lib/typescript/src/useJetMarkdownEditor.d.ts +36 -0
- package/lib/typescript/src/useJetMarkdownEditor.d.ts.map +1 -0
- package/package.json +150 -0
- package/react-native.config.js +11 -0
- package/src/JetMarkdownEditor.native.tsx +301 -0
- package/src/JetMarkdownEditor.tsx +90 -0
- package/src/JetMarkdownEditorNativeComponent.ts +173 -0
- package/src/JetMarkdownView.native.tsx +67 -0
- package/src/JetMarkdownView.tsx +16 -0
- package/src/JetMarkdownViewNativeComponent.ts +35 -0
- package/src/defaultStyles.ts +102 -0
- package/src/index.tsx +34 -0
- package/src/serializeStyles.ts +369 -0
- package/src/types.ts +438 -0
- 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
|