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,662 @@
|
|
|
1
|
+
// Golden tests for the shared markdown core.
|
|
2
|
+
// Build & run: cpp/tests/run_tests.sh
|
|
3
|
+
|
|
4
|
+
#include <algorithm>
|
|
5
|
+
#include <cstdio>
|
|
6
|
+
#include <string>
|
|
7
|
+
#include <utility>
|
|
8
|
+
#include <vector>
|
|
9
|
+
|
|
10
|
+
#include "../core/AstJson.h"
|
|
11
|
+
#include "../core/AstToMarkdown.h"
|
|
12
|
+
#include "../core/EditorRuns.h"
|
|
13
|
+
#include "../core/EditorText.h"
|
|
14
|
+
#include "../core/Parser.h"
|
|
15
|
+
|
|
16
|
+
namespace {
|
|
17
|
+
|
|
18
|
+
int g_failures = 0;
|
|
19
|
+
int g_total = 0;
|
|
20
|
+
|
|
21
|
+
void expectAst(const char* name, const std::string& markdown, const std::string& expected) {
|
|
22
|
+
g_total++;
|
|
23
|
+
auto doc = jetmarkdown::parseMarkdown(markdown);
|
|
24
|
+
const std::string actual = jetmarkdown::astToJson(doc->root);
|
|
25
|
+
if (actual != expected) {
|
|
26
|
+
g_failures++;
|
|
27
|
+
std::printf("FAIL %s\n input: %s\n expected: %s\n actual: %s\n",
|
|
28
|
+
name, markdown.c_str(), expected.c_str(), actual.c_str());
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
void expectContains(const char* name, const std::string& markdown, const std::string& fragment) {
|
|
33
|
+
g_total++;
|
|
34
|
+
auto doc = jetmarkdown::parseMarkdown(markdown);
|
|
35
|
+
const std::string actual = jetmarkdown::astToJson(doc->root);
|
|
36
|
+
if (actual.find(fragment) == std::string::npos) {
|
|
37
|
+
g_failures++;
|
|
38
|
+
std::printf("FAIL %s\n input: %s\n missing: %s\n actual: %s\n",
|
|
39
|
+
name, markdown.c_str(), fragment.c_str(), actual.c_str());
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
void expectNotContains(const char* name, const std::string& markdown, const std::string& fragment) {
|
|
44
|
+
g_total++;
|
|
45
|
+
auto doc = jetmarkdown::parseMarkdown(markdown);
|
|
46
|
+
const std::string actual = jetmarkdown::astToJson(doc->root);
|
|
47
|
+
if (actual.find(fragment) != std::string::npos) {
|
|
48
|
+
g_failures++;
|
|
49
|
+
std::printf("FAIL %s\n input: %s\n found unexpected: %s\n actual: %s\n",
|
|
50
|
+
name, markdown.c_str(), fragment.c_str(), actual.c_str());
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The round-trip law: parsing the serialized output must reproduce the
|
|
55
|
+
// original AST exactly, even when the spelling normalizes.
|
|
56
|
+
void expectRoundTrip(const char* name, const std::string& markdown) {
|
|
57
|
+
g_total++;
|
|
58
|
+
auto doc = jetmarkdown::parseMarkdown(markdown);
|
|
59
|
+
const std::string original = jetmarkdown::astToJson(doc->root);
|
|
60
|
+
const std::string serialized = jetmarkdown::astToMarkdown(doc->root);
|
|
61
|
+
auto reparsed = jetmarkdown::parseMarkdown(serialized);
|
|
62
|
+
const std::string actual = jetmarkdown::astToJson(reparsed->root);
|
|
63
|
+
if (actual != original) {
|
|
64
|
+
g_failures++;
|
|
65
|
+
std::printf(
|
|
66
|
+
"FAIL roundtrip %s\n input: %s\n serialized: %s\n expected: %s\n actual: %s\n",
|
|
67
|
+
name, markdown.c_str(), serialized.c_str(), original.c_str(), actual.c_str());
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
void expectString(
|
|
72
|
+
const char* name, const std::string& actual, const std::string& expected) {
|
|
73
|
+
g_total++;
|
|
74
|
+
if (actual != expected) {
|
|
75
|
+
g_failures++;
|
|
76
|
+
std::printf("FAIL %s\n expected: %s\n actual: %s\n",
|
|
77
|
+
name, expected.c_str(), actual.c_str());
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Canonical form: one maximal run per mark bit, so equivalent mark coverage
|
|
82
|
+
// compares equal regardless of how runs were sliced or combined.
|
|
83
|
+
std::string dumpStyled(const jetmarkdown::StyledText& styled) {
|
|
84
|
+
std::string out = "\"" + styled.text + "\"";
|
|
85
|
+
for (uint32_t bit = 1; bit <= jetmarkdown::MarkSubscript; bit <<= 1) {
|
|
86
|
+
std::vector<std::pair<uint32_t, uint32_t>> intervals;
|
|
87
|
+
for (const auto& run : styled.runs) {
|
|
88
|
+
if ((run.flags & bit) == 0 || run.start >= run.end) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
intervals.push_back({run.start, run.end});
|
|
92
|
+
}
|
|
93
|
+
std::sort(intervals.begin(), intervals.end());
|
|
94
|
+
for (size_t i = 0; i < intervals.size(); i++) {
|
|
95
|
+
auto [start, end] = intervals[i];
|
|
96
|
+
while (i + 1 < intervals.size() && intervals[i + 1].first <= end) {
|
|
97
|
+
end = std::max(end, intervals[++i].second);
|
|
98
|
+
}
|
|
99
|
+
out += " " + std::to_string(start) + ":" + std::to_string(end) + ":" +
|
|
100
|
+
std::to_string(bit);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
std::string dumpEditor(const jetmarkdown::EditorDocument& document) {
|
|
107
|
+
std::string out = dumpStyled({document.text, document.runs});
|
|
108
|
+
out += " |";
|
|
109
|
+
for (const auto& line : document.lines) {
|
|
110
|
+
out += " " + std::to_string(static_cast<int>(line.type));
|
|
111
|
+
if (line.level != 0) {
|
|
112
|
+
out += "." + std::to_string(line.level);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const auto& link : document.links) {
|
|
116
|
+
out += " [" + std::to_string(link.start) + ":" + std::to_string(link.end) +
|
|
117
|
+
" " + link.url + "]";
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// The editor round-trip law with blocks: text + runs + line blocks must
|
|
123
|
+
// survive serialization to markdown and re-extraction unchanged.
|
|
124
|
+
void expectEditorRoundTrip(
|
|
125
|
+
const char* name,
|
|
126
|
+
const std::string& text,
|
|
127
|
+
const std::vector<jetmarkdown::StyledRun>& runs,
|
|
128
|
+
const std::vector<jetmarkdown::EditorLine>& lines) {
|
|
129
|
+
g_total++;
|
|
130
|
+
const std::string markdown = jetmarkdown::markdownFromEditor(text, runs, lines);
|
|
131
|
+
const auto extracted = jetmarkdown::editorFromMarkdown(markdown);
|
|
132
|
+
const std::string expected = dumpEditor({text, runs, lines, {}});
|
|
133
|
+
const std::string actual = dumpEditor(extracted);
|
|
134
|
+
if (actual != expected) {
|
|
135
|
+
g_failures++;
|
|
136
|
+
std::printf(
|
|
137
|
+
"FAIL editor %s\n serialized: %s\n expected: %s\n actual: %s\n",
|
|
138
|
+
name, markdown.c_str(), expected.c_str(), actual.c_str());
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// The editor round-trip law: text + mark runs must survive serialization to
|
|
143
|
+
// markdown and re-extraction unchanged.
|
|
144
|
+
void expectStyledRoundTrip(
|
|
145
|
+
const char* name,
|
|
146
|
+
const std::string& text,
|
|
147
|
+
const std::vector<jetmarkdown::StyledRun>& runs) {
|
|
148
|
+
g_total++;
|
|
149
|
+
const std::string markdown = jetmarkdown::markdownFromStyledText(text, runs);
|
|
150
|
+
const auto extracted = jetmarkdown::styledTextFromMarkdown(markdown);
|
|
151
|
+
const std::string expected = dumpStyled({text, runs});
|
|
152
|
+
const std::string actual = dumpStyled(extracted);
|
|
153
|
+
if (actual != expected) {
|
|
154
|
+
g_failures++;
|
|
155
|
+
std::printf(
|
|
156
|
+
"FAIL styled %s\n serialized: %s\n expected: %s\n actual: %s\n",
|
|
157
|
+
name, markdown.c_str(), expected.c_str(), actual.c_str());
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
} // namespace
|
|
162
|
+
|
|
163
|
+
int main() {
|
|
164
|
+
using namespace std::string_literals;
|
|
165
|
+
|
|
166
|
+
// --- Basics ---
|
|
167
|
+
expectAst(
|
|
168
|
+
"paragraph",
|
|
169
|
+
"hello world",
|
|
170
|
+
R"({"type":"document","children":[{"type":"paragraph","children":[{"type":"text","text":"hello world"}]}]})");
|
|
171
|
+
|
|
172
|
+
expectAst(
|
|
173
|
+
"heading",
|
|
174
|
+
"## Title",
|
|
175
|
+
R"({"type":"document","children":[{"type":"heading","level":2,"children":[{"type":"text","text":"Title"}]}]})");
|
|
176
|
+
|
|
177
|
+
expectAst(
|
|
178
|
+
"bold italic nesting",
|
|
179
|
+
"**bold _italic_**",
|
|
180
|
+
R"({"type":"document","children":[{"type":"paragraph","children":[{"type":"bold","children":[{"type":"text","text":"bold "},{"type":"italic","children":[{"type":"text","text":"italic"}]}]}]}]})");
|
|
181
|
+
|
|
182
|
+
expectAst(
|
|
183
|
+
"link",
|
|
184
|
+
"[label](https://example.com)",
|
|
185
|
+
R"({"type":"document","children":[{"type":"paragraph","children":[{"type":"link","url":"https://example.com","children":[{"type":"text","text":"label"}]}]}]})");
|
|
186
|
+
|
|
187
|
+
expectContains(
|
|
188
|
+
"mention link keeps scheme",
|
|
189
|
+
"hey [@ali](users://ali)!",
|
|
190
|
+
R"({"type":"link","url":"users://ali","children":[{"type":"text","text":"@ali"}]})");
|
|
191
|
+
|
|
192
|
+
expectContains(
|
|
193
|
+
"autolink",
|
|
194
|
+
"see https://example.com now",
|
|
195
|
+
R"("type":"link","url":"https://example.com")");
|
|
196
|
+
|
|
197
|
+
expectAst(
|
|
198
|
+
"image",
|
|
199
|
+
"",
|
|
200
|
+
R"({"type":"document","children":[{"type":"paragraph","children":[{"type":"image","text":"alt text","url":"https://example.com/x.png"}]}]})");
|
|
201
|
+
|
|
202
|
+
expectAst(
|
|
203
|
+
"inline code",
|
|
204
|
+
"run `npm i` now",
|
|
205
|
+
R"({"type":"document","children":[{"type":"paragraph","children":[{"type":"text","text":"run "},{"type":"inlineCode","text":"npm i"},{"type":"text","text":" now"}]}]})");
|
|
206
|
+
|
|
207
|
+
expectAst(
|
|
208
|
+
"code block with lang",
|
|
209
|
+
"```js\nlet x = 1;\n```",
|
|
210
|
+
R"({"type":"document","children":[{"type":"codeBlock","text":"let x = 1;\n","url":"js"}]})");
|
|
211
|
+
|
|
212
|
+
expectContains("block quote", "> quoted", R"("type":"blockQuote")");
|
|
213
|
+
|
|
214
|
+
expectContains(
|
|
215
|
+
"ordered list start",
|
|
216
|
+
"3. three\n4. four",
|
|
217
|
+
R"("type":"list","ordered":true,"start":3)");
|
|
218
|
+
|
|
219
|
+
expectContains("thematic break", "a\n\n---\n\nb", R"("type":"thematicBreak")");
|
|
220
|
+
|
|
221
|
+
expectContains("hard break", "line one \nline two", R"("type":"hardBreak")");
|
|
222
|
+
|
|
223
|
+
// --- Tables ---
|
|
224
|
+
expectAst(
|
|
225
|
+
"table structure",
|
|
226
|
+
"| a | b |\n|---|--:|\n| 1 | 2 |",
|
|
227
|
+
R"({"type":"document","children":[{"type":"table","children":[{"type":"tableRow","level":1,"children":[{"type":"tableCell","children":[{"type":"text","text":"a"}]},{"type":"tableCell","level":3,"children":[{"type":"text","text":"b"}]}]},{"type":"tableRow","children":[{"type":"tableCell","children":[{"type":"text","text":"1"}]},{"type":"tableCell","level":3,"children":[{"type":"text","text":"2"}]}]}]}]})");
|
|
228
|
+
|
|
229
|
+
// --- Spoilers ---
|
|
230
|
+
expectAst(
|
|
231
|
+
"discord spoiler",
|
|
232
|
+
"a ||secret|| b",
|
|
233
|
+
R"({"type":"document","children":[{"type":"paragraph","children":[{"type":"text","text":"a "},{"type":"spoiler","children":[{"type":"text","text":"secret"}]},{"type":"text","text":" b"}]}]})");
|
|
234
|
+
|
|
235
|
+
expectAst(
|
|
236
|
+
"reddit spoiler mid-line",
|
|
237
|
+
"a >!secret!< b",
|
|
238
|
+
R"({"type":"document","children":[{"type":"paragraph","children":[{"type":"text","text":"a "},{"type":"spoiler","children":[{"type":"text","text":"secret"}]},{"type":"text","text":" b"}]}]})");
|
|
239
|
+
|
|
240
|
+
expectAst(
|
|
241
|
+
"reddit spoiler at line start (not blockquote)",
|
|
242
|
+
">!secret!< after",
|
|
243
|
+
R"({"type":"document","children":[{"type":"paragraph","children":[{"type":"spoiler","children":[{"type":"text","text":"secret"}]},{"type":"text","text":" after"}]}]})");
|
|
244
|
+
|
|
245
|
+
expectContains(
|
|
246
|
+
"spoiler inside blockquote",
|
|
247
|
+
"> before >!secret!< after",
|
|
248
|
+
R"({"type":"spoiler","children":[{"type":"text","text":"secret"}]})");
|
|
249
|
+
|
|
250
|
+
expectAst(
|
|
251
|
+
"spoiler spanning styled runs",
|
|
252
|
+
"||bold **x** end||",
|
|
253
|
+
R"({"type":"document","children":[{"type":"paragraph","children":[{"type":"spoiler","children":[{"type":"text","text":"bold "},{"type":"bold","children":[{"type":"text","text":"x"}]},{"type":"text","text":" end"}]}]}]})");
|
|
254
|
+
|
|
255
|
+
expectNotContains(
|
|
256
|
+
"spoiler not inside fenced code",
|
|
257
|
+
"```\n>!not a spoiler!<\n||also not||\n```",
|
|
258
|
+
R"("type":"spoiler")");
|
|
259
|
+
|
|
260
|
+
expectNotContains(
|
|
261
|
+
"spoiler not inside inline code",
|
|
262
|
+
"`||nope||`",
|
|
263
|
+
R"("type":"spoiler")");
|
|
264
|
+
|
|
265
|
+
expectNotContains("empty spoiler literal", "||||", R"("type":"spoiler")");
|
|
266
|
+
|
|
267
|
+
expectNotContains(
|
|
268
|
+
"unclosed spoiler literal",
|
|
269
|
+
"a ||secret forever",
|
|
270
|
+
R"("type":"spoiler")");
|
|
271
|
+
|
|
272
|
+
// --- Strikethrough vs subscript ---
|
|
273
|
+
expectAst(
|
|
274
|
+
"double tilde strikethrough",
|
|
275
|
+
"~~gone~~",
|
|
276
|
+
R"({"type":"document","children":[{"type":"paragraph","children":[{"type":"strikethrough","children":[{"type":"text","text":"gone"}]}]}]})");
|
|
277
|
+
|
|
278
|
+
expectAst(
|
|
279
|
+
"single tilde subscript",
|
|
280
|
+
"H~2~O",
|
|
281
|
+
R"({"type":"document","children":[{"type":"paragraph","children":[{"type":"text","text":"H"},{"type":"subscript","children":[{"type":"text","text":"2"}]},{"type":"text","text":"O"}]}]})");
|
|
282
|
+
|
|
283
|
+
expectNotContains(
|
|
284
|
+
"subscript rejects spaces",
|
|
285
|
+
"~not a sub~",
|
|
286
|
+
R"("type":"subscript")");
|
|
287
|
+
|
|
288
|
+
expectContains(
|
|
289
|
+
"strikethrough spanning styled runs",
|
|
290
|
+
"~~a **b** c~~",
|
|
291
|
+
R"({"type":"strikethrough","children":[{"type":"text","text":"a "},{"type":"bold","children":[{"type":"text","text":"b"}]},{"type":"text","text":" c"}]})");
|
|
292
|
+
|
|
293
|
+
// --- Superscript ---
|
|
294
|
+
expectAst(
|
|
295
|
+
"pandoc superscript",
|
|
296
|
+
"x^2^ y",
|
|
297
|
+
R"({"type":"document","children":[{"type":"paragraph","children":[{"type":"text","text":"x"},{"type":"superscript","children":[{"type":"text","text":"2"}]},{"type":"text","text":" y"}]}]})");
|
|
298
|
+
|
|
299
|
+
expectAst(
|
|
300
|
+
"reddit bare superscript",
|
|
301
|
+
"x^2 y",
|
|
302
|
+
R"({"type":"document","children":[{"type":"paragraph","children":[{"type":"text","text":"x"},{"type":"superscript","children":[{"type":"text","text":"2"}]},{"type":"text","text":" y"}]}]})");
|
|
303
|
+
|
|
304
|
+
expectAst(
|
|
305
|
+
"reddit paren superscript",
|
|
306
|
+
"note^(multi word here) end",
|
|
307
|
+
R"({"type":"document","children":[{"type":"paragraph","children":[{"type":"text","text":"note"},{"type":"superscript","children":[{"type":"text","text":"multi word here"}]},{"type":"text","text":" end"}]}]})");
|
|
308
|
+
|
|
309
|
+
expectAst(
|
|
310
|
+
"bare superscript to end of line",
|
|
311
|
+
"wow^amazing",
|
|
312
|
+
R"({"type":"document","children":[{"type":"paragraph","children":[{"type":"text","text":"wow"},{"type":"superscript","children":[{"type":"text","text":"amazing"}]}]}]})");
|
|
313
|
+
|
|
314
|
+
expectNotContains(
|
|
315
|
+
"lone caret literal",
|
|
316
|
+
"a ^ b",
|
|
317
|
+
R"("type":"superscript")");
|
|
318
|
+
|
|
319
|
+
// --- Interactions ---
|
|
320
|
+
expectContains(
|
|
321
|
+
"sub inside spoiler",
|
|
322
|
+
"||H~2~O||",
|
|
323
|
+
R"({"type":"spoiler","children":[{"type":"text","text":"H"},{"type":"subscript","children":[{"type":"text","text":"2"}]},{"type":"text","text":"O"}]})");
|
|
324
|
+
|
|
325
|
+
expectNotContains(
|
|
326
|
+
"extensions skip link labels",
|
|
327
|
+
"[a ||b|| c](https://example.com)",
|
|
328
|
+
R"("type":"spoiler")");
|
|
329
|
+
|
|
330
|
+
expectContains(
|
|
331
|
+
"entity translation",
|
|
332
|
+
"a & b A",
|
|
333
|
+
R"({"type":"text","text":"a & b A"})");
|
|
334
|
+
|
|
335
|
+
expectContains(
|
|
336
|
+
"nested list structure",
|
|
337
|
+
"- a\n - b",
|
|
338
|
+
R"("type":"list")");
|
|
339
|
+
|
|
340
|
+
// --- Serializer round trips (AstToMarkdown) ---
|
|
341
|
+
expectRoundTrip("rt paragraph", "hello world");
|
|
342
|
+
expectRoundTrip("rt two paragraphs", "one\n\ntwo");
|
|
343
|
+
expectRoundTrip("rt soft break", "line one\nline two");
|
|
344
|
+
expectRoundTrip("rt hard break", "line one\\\nline two");
|
|
345
|
+
expectRoundTrip("rt headings", "# H1\n\n## H2\n\n###### H6");
|
|
346
|
+
expectRoundTrip("rt bold italic", "**bold** and _italic_ and **bold _nested_**");
|
|
347
|
+
expectRoundTrip("rt strikethrough", "~~gone~~ stays");
|
|
348
|
+
expectRoundTrip("rt spoiler", "both ||secret|| kinds >!hidden!< here");
|
|
349
|
+
expectRoundTrip("rt sup sub", "x^2^ and H~2~O and reddit ^word too");
|
|
350
|
+
expectRoundTrip("rt sup multiword", "reddit ^(multi word) form");
|
|
351
|
+
expectRoundTrip("rt inline code", "run `npm install` now");
|
|
352
|
+
expectRoundTrip("rt inline code with backticks", "a ``code `with` ticks`` b");
|
|
353
|
+
expectRoundTrip("rt link", "see [the docs](https://example.com) now");
|
|
354
|
+
expectRoundTrip("rt link with parens", "see [x](https://en.wikipedia.org/wiki/Bracket_(disambiguation))");
|
|
355
|
+
expectRoundTrip("rt mention", "ping [@ali](users://ali) in [#general](channels://general)");
|
|
356
|
+
expectRoundTrip("rt autolink", "visit https://example.com today");
|
|
357
|
+
{
|
|
358
|
+
// A link whose label equals its URL serializes as the bare URL (the
|
|
359
|
+
// permissive-autolink form), not the noisy [url](url) form.
|
|
360
|
+
auto doc = jetmarkdown::parseMarkdown("visit https://example.com today");
|
|
361
|
+
expectString("autolink serializes bare",
|
|
362
|
+
jetmarkdown::astToMarkdown(doc->root),
|
|
363
|
+
"visit https://example.com today\n");
|
|
364
|
+
auto labeled = jetmarkdown::parseMarkdown("[docs](https://example.com)");
|
|
365
|
+
expectString("labeled link keeps brackets",
|
|
366
|
+
jetmarkdown::astToMarkdown(labeled->root),
|
|
367
|
+
"[docs](https://example.com)\n");
|
|
368
|
+
}
|
|
369
|
+
expectRoundTrip("rt image", "");
|
|
370
|
+
expectRoundTrip("rt quote", "> quoted **bold**\n>\n> second paragraph");
|
|
371
|
+
expectRoundTrip("rt nested quote content", "> outer\n>\n> - a\n> - b");
|
|
372
|
+
expectRoundTrip("rt code block", "```ts\nconst x = 1;\nconst y = \"two\";\n```");
|
|
373
|
+
expectRoundTrip("rt code block with backticks", "````\na ``` fence inside\n````");
|
|
374
|
+
expectRoundTrip("rt unordered list", "- one\n- two\n- three");
|
|
375
|
+
expectRoundTrip("rt ordered list", "1. first\n2. second");
|
|
376
|
+
expectRoundTrip("rt ordered list start", "5. five\n6. six");
|
|
377
|
+
expectRoundTrip("rt nested list", "- a\n - a1\n - a2\n- b");
|
|
378
|
+
expectRoundTrip("rt list with paragraphs", "- first para\n\n second para\n- next item");
|
|
379
|
+
expectRoundTrip("rt table", "| A | B |\n|---|---|\n| 1 | 2 |");
|
|
380
|
+
expectRoundTrip("rt table alignment", "| L | C | R |\n|:--|:-:|--:|\n| a | b | c |");
|
|
381
|
+
expectRoundTrip("rt thematic break", "one\n\n---\n\ntwo");
|
|
382
|
+
expectRoundTrip("rt literal specials", "not *bold* stays\\* literal \\_x\\_ and \\`tick\\`");
|
|
383
|
+
expectRoundTrip("rt literal hash", "\\# not a heading");
|
|
384
|
+
expectRoundTrip("rt literal list", "1\\. not a list");
|
|
385
|
+
expectRoundTrip("rt literal pipe", "a \\| b");
|
|
386
|
+
expectRoundTrip("rt exclamation", "wow! and .\n\n"
|
|
389
|
+
"> A quote\n\n- item one\n- item two\n\n```js\nconsole.log(1);\n```\n\n"
|
|
390
|
+
"| K | V |\n|---|---|\n| a | 1 |\n\n---\n\nThe ||end|| ^fin^");
|
|
391
|
+
|
|
392
|
+
// --- Escaped extension delimiters must stay literal through round trips
|
|
393
|
+
// (md4c strips backslashes before the inline-extension scanner runs; the
|
|
394
|
+
// preprocess entity rewrite + verbatim text nodes keep them de-fanged).
|
|
395
|
+
expectStyledRoundTrip("escape literal spoiler", "a ||spoiler|| b", {});
|
|
396
|
+
expectStyledRoundTrip("escape literal caret", "a^b c", {});
|
|
397
|
+
expectStyledRoundTrip("escape literal tilde pair", "a ~sub~ b", {});
|
|
398
|
+
expectStyledRoundTrip("escape literal strikethrough", "x ~~strike~~ y", {});
|
|
399
|
+
expectStyledRoundTrip("escape literal reddit spoiler", ">!hidden!< tail", {});
|
|
400
|
+
expectStyledRoundTrip("escape literal entity text", "use | here", {});
|
|
401
|
+
expectStyledRoundTrip("escape literal angle autolink", "see <http://example.com> now", {});
|
|
402
|
+
expectStyledRoundTrip("escape leading spaces", " indented text", {});
|
|
403
|
+
expectRoundTrip("escaped pipe still literal in backticks", "keep `a \\| b` code");
|
|
404
|
+
|
|
405
|
+
// --- Emphasis adjacency: stars must survive intraword adjacency and the
|
|
406
|
+
// 4+ star-run fallback must use flanking-legal underscores.
|
|
407
|
+
expectStyledRoundTrip("adjacent bold italic intraword", "XyZ",
|
|
408
|
+
{{0, 1, jetmarkdown::MarkBold}, {1, 2, jetmarkdown::MarkItalic}});
|
|
409
|
+
expectStyledRoundTrip("adjacent italic bold intraword", "XyZ",
|
|
410
|
+
{{0, 1, jetmarkdown::MarkItalic}, {1, 2, jetmarkdown::MarkBold}});
|
|
411
|
+
|
|
412
|
+
// --- Superscript content that fits neither caret form.
|
|
413
|
+
expectStyledRoundTrip("sup paren without space", "ab",
|
|
414
|
+
{{0, 2, jetmarkdown::MarkSuperscript}});
|
|
415
|
+
|
|
416
|
+
// --- Hostile nesting must parse without blowing the stack (depth cap).
|
|
417
|
+
{
|
|
418
|
+
std::string hostile;
|
|
419
|
+
for (int i = 0; i < 20000; i++) {
|
|
420
|
+
hostile += '>';
|
|
421
|
+
}
|
|
422
|
+
hostile += " x";
|
|
423
|
+
auto doc = jetmarkdown::parseMarkdown(hostile);
|
|
424
|
+
g_total++;
|
|
425
|
+
if (doc->root == nullptr) {
|
|
426
|
+
g_failures++;
|
|
427
|
+
std::printf("FAIL deep nesting parse\n");
|
|
428
|
+
} else {
|
|
429
|
+
// Serialization must also complete (bounded recursion).
|
|
430
|
+
const std::string out = jetmarkdown::astToMarkdown(doc->root);
|
|
431
|
+
if (out.empty()) {
|
|
432
|
+
g_failures++;
|
|
433
|
+
std::printf("FAIL deep nesting serialize\n");
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// --- Nested image inside alt text keeps the outer alt intact.
|
|
439
|
+
expectContains(
|
|
440
|
+
"nested image alt",
|
|
441
|
+
" c](u1)",
|
|
442
|
+
R"("text":"a b c")");
|
|
443
|
+
|
|
444
|
+
// --- Surrogate-range character references become U+FFFD, not invalid
|
|
445
|
+
// UTF-8.
|
|
446
|
+
expectContains(
|
|
447
|
+
"surrogate entity replaced",
|
|
448
|
+
"bad � ref",
|
|
449
|
+
"\xEF\xBF\xBD");
|
|
450
|
+
|
|
451
|
+
// --- Editor plain-text bridge (E1) ---
|
|
452
|
+
expectString(
|
|
453
|
+
"editor markdown from text",
|
|
454
|
+
jetmarkdown::markdownFromPlainText("hello\nworld"),
|
|
455
|
+
"hello\n\nworld\n");
|
|
456
|
+
expectString(
|
|
457
|
+
"editor text escapes literals",
|
|
458
|
+
jetmarkdown::markdownFromPlainText("**not bold** #tag"),
|
|
459
|
+
"\\*\\*not bold\\*\\* #tag\n");
|
|
460
|
+
expectString(
|
|
461
|
+
"editor text escapes line starts",
|
|
462
|
+
jetmarkdown::markdownFromPlainText("# not a heading\n1. not a list"),
|
|
463
|
+
"\\# not a heading\n\n1\\. not a list\n");
|
|
464
|
+
expectString(
|
|
465
|
+
"editor text stable round trip",
|
|
466
|
+
jetmarkdown::plainTextFromMarkdown(
|
|
467
|
+
jetmarkdown::markdownFromPlainText("line one\nline two")),
|
|
468
|
+
"line one\nline two");
|
|
469
|
+
expectString(
|
|
470
|
+
"editor setValue flattens structure",
|
|
471
|
+
jetmarkdown::plainTextFromMarkdown("# Title\n\nbody with **bold**\n\n- a\n- b"),
|
|
472
|
+
"Title\nbody with bold\na\nb");
|
|
473
|
+
|
|
474
|
+
// --- Editor styled runs (E2) ---
|
|
475
|
+
using jetmarkdown::MarkBold;
|
|
476
|
+
using jetmarkdown::MarkInlineCode;
|
|
477
|
+
using jetmarkdown::MarkItalic;
|
|
478
|
+
using jetmarkdown::MarkSpoiler;
|
|
479
|
+
using jetmarkdown::MarkStrikethrough;
|
|
480
|
+
using jetmarkdown::MarkSubscript;
|
|
481
|
+
using jetmarkdown::MarkSuperscript;
|
|
482
|
+
|
|
483
|
+
expectString(
|
|
484
|
+
"styled bold run",
|
|
485
|
+
jetmarkdown::markdownFromStyledText("hello world", {{0, 5, MarkBold}}),
|
|
486
|
+
"**hello** world\n");
|
|
487
|
+
expectString(
|
|
488
|
+
"styled code ignores nested marks",
|
|
489
|
+
jetmarkdown::markdownFromStyledText(
|
|
490
|
+
"x code y", {{2, 6, MarkInlineCode | MarkBold}}),
|
|
491
|
+
"x **`code`** y\n");
|
|
492
|
+
expectString(
|
|
493
|
+
"styled run clipped at newline",
|
|
494
|
+
jetmarkdown::markdownFromStyledText("ab\ncd", {{0, 5, MarkBold}}),
|
|
495
|
+
"**ab**\n\n**cd**\n");
|
|
496
|
+
expectString(
|
|
497
|
+
"styled marks escape content",
|
|
498
|
+
jetmarkdown::markdownFromStyledText("a*b", {{0, 3, MarkBold}}),
|
|
499
|
+
"**a\\*b**\n");
|
|
500
|
+
|
|
501
|
+
expectStyledRoundTrip("styled rt plain", "just text", {});
|
|
502
|
+
expectStyledRoundTrip("styled rt bold", "hello world", {{0, 5, MarkBold}});
|
|
503
|
+
expectStyledRoundTrip(
|
|
504
|
+
"styled rt every mark",
|
|
505
|
+
"abcdefg",
|
|
506
|
+
{{0, 1, MarkBold},
|
|
507
|
+
{1, 2, MarkItalic},
|
|
508
|
+
{2, 3, MarkStrikethrough},
|
|
509
|
+
{3, 4, MarkInlineCode},
|
|
510
|
+
{4, 5, MarkSpoiler},
|
|
511
|
+
{5, 6, MarkSuperscript},
|
|
512
|
+
{6, 7, MarkSubscript}});
|
|
513
|
+
expectStyledRoundTrip(
|
|
514
|
+
"styled rt overlap", "abc", {{0, 2, MarkBold}, {1, 3, MarkItalic}});
|
|
515
|
+
expectStyledRoundTrip(
|
|
516
|
+
"styled rt overlap glued tail",
|
|
517
|
+
"bcd",
|
|
518
|
+
{{0, 1, MarkBold}, {0, 2, MarkItalic}});
|
|
519
|
+
expectStyledRoundTrip(
|
|
520
|
+
"styled rt partial bold in code",
|
|
521
|
+
"x bc y",
|
|
522
|
+
{{2, 4, MarkInlineCode}, {3, 4, MarkBold}});
|
|
523
|
+
expectStyledRoundTrip(
|
|
524
|
+
"styled rt nested combo",
|
|
525
|
+
"bold and italic",
|
|
526
|
+
{{0, 15, MarkBold}, {9, 15, MarkItalic}});
|
|
527
|
+
expectStyledRoundTrip(
|
|
528
|
+
"styled rt multiline",
|
|
529
|
+
"first line\nsecond line",
|
|
530
|
+
{{0, 5, MarkBold}, {11, 17, MarkStrikethrough}});
|
|
531
|
+
expectStyledRoundTrip(
|
|
532
|
+
"styled rt emoji offsets", "\xF0\x9F\x98\x80 xy", {{3, 5, MarkBold}});
|
|
533
|
+
expectStyledRoundTrip(
|
|
534
|
+
"styled rt literal specials", "keep **this** literal", {{5, 13, MarkSpoiler}});
|
|
535
|
+
expectStyledRoundTrip(
|
|
536
|
+
"styled rt adjacent same mark merge", "abc", {{0, 2, MarkBold}, {2, 3, MarkBold}});
|
|
537
|
+
|
|
538
|
+
expectString(
|
|
539
|
+
"styled extraction",
|
|
540
|
+
dumpStyled(jetmarkdown::styledTextFromMarkdown("a **b** `c`")),
|
|
541
|
+
"\"a b c\" 2:3:1 4:5:8");
|
|
542
|
+
|
|
543
|
+
expectString(
|
|
544
|
+
"styled run trims edge spaces",
|
|
545
|
+
jetmarkdown::markdownFromStyledText("hi bold", {{2, 7, MarkBold}}),
|
|
546
|
+
"hi **bold**\n");
|
|
547
|
+
expectString(
|
|
548
|
+
"styled whitespace-only run drops",
|
|
549
|
+
jetmarkdown::markdownFromStyledText("a b", {{1, 2, MarkBold}}),
|
|
550
|
+
"a b\n");
|
|
551
|
+
// The pad space survives the code-span round trip (CommonMark strips one
|
|
552
|
+
// leading/trailing space pair), so the content keeps its edge spaces.
|
|
553
|
+
expectStyledRoundTrip(
|
|
554
|
+
"styled code run keeps spaces", "x y z", {{1, 4, MarkInlineCode}});
|
|
555
|
+
|
|
556
|
+
// --- Editor line blocks (E3) ---
|
|
557
|
+
using jetmarkdown::EditorBlockType;
|
|
558
|
+
using jetmarkdown::EditorLine;
|
|
559
|
+
const EditorLine P = {EditorBlockType::Paragraph, 0};
|
|
560
|
+
const EditorLine Q = {EditorBlockType::Quote, 0};
|
|
561
|
+
const EditorLine C = {EditorBlockType::Code, 0};
|
|
562
|
+
const EditorLine UL = {EditorBlockType::Bullet, 0};
|
|
563
|
+
const EditorLine OL = {EditorBlockType::Ordered, 0};
|
|
564
|
+
|
|
565
|
+
expectString(
|
|
566
|
+
"editor heading line",
|
|
567
|
+
jetmarkdown::markdownFromEditor(
|
|
568
|
+
"Title\nbody", {}, {{EditorBlockType::Heading, 2}, P}),
|
|
569
|
+
"## Title\n\nbody\n");
|
|
570
|
+
expectString(
|
|
571
|
+
"editor quote lines merge",
|
|
572
|
+
jetmarkdown::markdownFromEditor("a\nb", {}, {Q, Q}),
|
|
573
|
+
"> a\n>\n> b\n");
|
|
574
|
+
expectString(
|
|
575
|
+
"editor code lines merge raw",
|
|
576
|
+
jetmarkdown::markdownFromEditor(
|
|
577
|
+
"x = 1\ny *= 2", {{0, 5, MarkBold}}, {C, C}),
|
|
578
|
+
"```\nx = 1\ny *= 2\n```\n");
|
|
579
|
+
expectString(
|
|
580
|
+
"editor bullet list",
|
|
581
|
+
jetmarkdown::markdownFromEditor("a\nb", {}, {UL, UL}),
|
|
582
|
+
"- a\n- b\n");
|
|
583
|
+
expectString(
|
|
584
|
+
"editor ordered list",
|
|
585
|
+
jetmarkdown::markdownFromEditor("a\nb", {}, {OL, OL}),
|
|
586
|
+
"1. a\n2. b\n");
|
|
587
|
+
expectString(
|
|
588
|
+
"editor mixed blocks",
|
|
589
|
+
jetmarkdown::markdownFromEditor(
|
|
590
|
+
"Title\nintro\nitem", {{0, 5, MarkBold}},
|
|
591
|
+
{{EditorBlockType::Heading, 1}, P, UL}),
|
|
592
|
+
"# **Title**\n\nintro\n\n- item\n");
|
|
593
|
+
|
|
594
|
+
expectEditorRoundTrip("editor rt paragraphs", "one\ntwo", {}, {P, P});
|
|
595
|
+
expectEditorRoundTrip(
|
|
596
|
+
"editor rt heading levels",
|
|
597
|
+
"h1\nh6",
|
|
598
|
+
{},
|
|
599
|
+
{{EditorBlockType::Heading, 1}, {EditorBlockType::Heading, 6}});
|
|
600
|
+
expectEditorRoundTrip("editor rt quote", "quoted\nlines", {}, {Q, Q});
|
|
601
|
+
expectEditorRoundTrip(
|
|
602
|
+
"editor rt code block", "const x = 1;\nreturn x;", {}, {C, C});
|
|
603
|
+
expectEditorRoundTrip("editor rt bullets", "a\nb\nc", {}, {UL, UL, UL});
|
|
604
|
+
expectEditorRoundTrip("editor rt ordered", "a\nb", {}, {OL, OL});
|
|
605
|
+
expectEditorRoundTrip(
|
|
606
|
+
"editor rt marked list items",
|
|
607
|
+
"bold item\nplain",
|
|
608
|
+
{{0, 4, MarkBold}},
|
|
609
|
+
{UL, UL});
|
|
610
|
+
expectEditorRoundTrip(
|
|
611
|
+
"editor rt block transitions",
|
|
612
|
+
"head\npara\nquote\ncode\nitem one\nitem two\ntail",
|
|
613
|
+
{},
|
|
614
|
+
{{EditorBlockType::Heading, 3}, P, Q, C, UL, UL, P});
|
|
615
|
+
|
|
616
|
+
expectString(
|
|
617
|
+
"editor extraction with blocks",
|
|
618
|
+
dumpEditor(jetmarkdown::editorFromMarkdown(
|
|
619
|
+
"# Title\n\n- a\n- **b**\n\n> q\n\n```\nx\n```")),
|
|
620
|
+
"\"Title\na\nb\nq\nx\" 8:9:1 | 1.1 4 4 2 3");
|
|
621
|
+
|
|
622
|
+
// --- Editor links + mentions (E4) ---
|
|
623
|
+
expectString(
|
|
624
|
+
"editor link run serializes",
|
|
625
|
+
jetmarkdown::markdownFromEditor(
|
|
626
|
+
"visit the docs now", {}, {}, {{6, 14, "https://x.dev"}}),
|
|
627
|
+
"visit [the docs](https://x.dev) now\n");
|
|
628
|
+
expectString(
|
|
629
|
+
"editor mention link",
|
|
630
|
+
jetmarkdown::markdownFromEditor(
|
|
631
|
+
"ping @ali ok", {}, {}, {{5, 9, "users://ali"}}),
|
|
632
|
+
"ping [@ali](users://ali) ok\n");
|
|
633
|
+
expectString(
|
|
634
|
+
"editor marked link label",
|
|
635
|
+
jetmarkdown::markdownFromEditor(
|
|
636
|
+
"see docs here",
|
|
637
|
+
{{4, 8, jetmarkdown::MarkBold}},
|
|
638
|
+
{},
|
|
639
|
+
{{4, 8, "https://x.dev"}}),
|
|
640
|
+
"see [**docs**](https://x.dev) here\n");
|
|
641
|
+
expectString(
|
|
642
|
+
"editor link extraction",
|
|
643
|
+
dumpEditor(jetmarkdown::editorFromMarkdown(
|
|
644
|
+
"visit [the docs](https://x.dev) and [@ali](users://ali)")),
|
|
645
|
+
"\"visit the docs and @ali\" | 0 [6:14 https://x.dev] [19:23 users://ali]");
|
|
646
|
+
expectString(
|
|
647
|
+
"editor autolink extraction",
|
|
648
|
+
dumpEditor(jetmarkdown::editorFromMarkdown("see https://x.dev now")),
|
|
649
|
+
"\"see https://x.dev now\" | 0 [4:17 https://x.dev]");
|
|
650
|
+
expectString(
|
|
651
|
+
"editor link in list item",
|
|
652
|
+
jetmarkdown::markdownFromEditor(
|
|
653
|
+
"docs\nother",
|
|
654
|
+
{},
|
|
655
|
+
{{jetmarkdown::EditorBlockType::Bullet, 0},
|
|
656
|
+
{jetmarkdown::EditorBlockType::Bullet, 0}},
|
|
657
|
+
{{0, 4, "https://x.dev"}}),
|
|
658
|
+
"- [docs](https://x.dev)\n- other\n");
|
|
659
|
+
|
|
660
|
+
std::printf("%d/%d passed\n", g_total - g_failures, g_total);
|
|
661
|
+
return g_failures == 0 ? 0 : 1;
|
|
662
|
+
}
|