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,470 @@
|
|
|
1
|
+
#include "Parser.h"
|
|
2
|
+
|
|
3
|
+
#include <cstdlib>
|
|
4
|
+
|
|
5
|
+
#include "../md4c/md4c.h"
|
|
6
|
+
#include "InlineExtensions.h"
|
|
7
|
+
#include "Preprocess.h"
|
|
8
|
+
|
|
9
|
+
namespace jetmarkdown {
|
|
10
|
+
|
|
11
|
+
namespace {
|
|
12
|
+
|
|
13
|
+
void appendUtf8(std::string& out, uint32_t cp) {
|
|
14
|
+
// Surrogates are not scalar values; encoding them yields invalid UTF-8
|
|
15
|
+
// that desynchronizes the UTF-16 offset tables downstream.
|
|
16
|
+
if (cp == 0 || cp > 0x10FFFF || (cp >= 0xD800 && cp <= 0xDFFF)) {
|
|
17
|
+
cp = 0xFFFD;
|
|
18
|
+
}
|
|
19
|
+
if (cp < 0x80) {
|
|
20
|
+
out.push_back(static_cast<char>(cp));
|
|
21
|
+
} else if (cp < 0x800) {
|
|
22
|
+
out.push_back(static_cast<char>(0xC0 | (cp >> 6)));
|
|
23
|
+
out.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
|
|
24
|
+
} else if (cp < 0x10000) {
|
|
25
|
+
out.push_back(static_cast<char>(0xE0 | (cp >> 12)));
|
|
26
|
+
out.push_back(static_cast<char>(0x80 | ((cp >> 6) & 0x3F)));
|
|
27
|
+
out.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
|
|
28
|
+
} else {
|
|
29
|
+
out.push_back(static_cast<char>(0xF0 | (cp >> 18)));
|
|
30
|
+
out.push_back(static_cast<char>(0x80 | ((cp >> 12) & 0x3F)));
|
|
31
|
+
out.push_back(static_cast<char>(0x80 | ((cp >> 6) & 0x3F)));
|
|
32
|
+
out.push_back(static_cast<char>(0x80 | (cp & 0x3F)));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Translates one markdown entity ("&", "A", "A") to UTF-8.
|
|
37
|
+
// Unknown named entities are appended literally.
|
|
38
|
+
void appendEntity(std::string& out, const char* s, size_t n) {
|
|
39
|
+
if (n < 3 || s[0] != '&' || s[n - 1] != ';') {
|
|
40
|
+
out.append(s, n);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (s[1] == '#') {
|
|
44
|
+
uint32_t cp = 0;
|
|
45
|
+
bool hex = (n > 3 && (s[2] == 'x' || s[2] == 'X'));
|
|
46
|
+
size_t i = hex ? 3 : 2;
|
|
47
|
+
if (i >= n - 1) {
|
|
48
|
+
out.append(s, n);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
for (; i < n - 1; i++) {
|
|
52
|
+
char c = s[i];
|
|
53
|
+
uint32_t digit;
|
|
54
|
+
if (c >= '0' && c <= '9') {
|
|
55
|
+
digit = c - '0';
|
|
56
|
+
} else if (hex && c >= 'a' && c <= 'f') {
|
|
57
|
+
digit = 10 + (c - 'a');
|
|
58
|
+
} else if (hex && c >= 'A' && c <= 'F') {
|
|
59
|
+
digit = 10 + (c - 'A');
|
|
60
|
+
} else {
|
|
61
|
+
out.append(s, n);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
cp = cp * (hex ? 16 : 10) + digit;
|
|
65
|
+
if (cp > 0x10FFFF) {
|
|
66
|
+
cp = 0xFFFD;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
appendUtf8(out, cp);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const std::string name(s + 1, n - 2);
|
|
73
|
+
struct Named {
|
|
74
|
+
const char* name;
|
|
75
|
+
const char* utf8;
|
|
76
|
+
};
|
|
77
|
+
static const Named kNamed[] = {
|
|
78
|
+
{"amp", "&"}, {"lt", "<"}, {"gt", ">"},
|
|
79
|
+
{"quot", "\""}, {"apos", "'"}, {"nbsp", " "},
|
|
80
|
+
{"copy", "©"},{"reg", "®"}, {"trade", "™"},
|
|
81
|
+
{"hellip", "…"}, {"mdash", "—"}, {"ndash", "–"},
|
|
82
|
+
{"lsquo", "‘"}, {"rsquo", "’"}, {"ldquo", "“"},
|
|
83
|
+
{"rdquo", "”"}, {"deg", "°"},
|
|
84
|
+
};
|
|
85
|
+
for (const auto& e : kNamed) {
|
|
86
|
+
if (name == e.name) {
|
|
87
|
+
out.append(e.utf8);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
out.append(s, n);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
std::string attributeToString(const MD_ATTRIBUTE& attr) {
|
|
95
|
+
std::string out;
|
|
96
|
+
if (attr.text == nullptr || attr.size == 0) {
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
for (int i = 0; attr.substr_offsets[i] < attr.size; i++) {
|
|
100
|
+
MD_OFFSET off = attr.substr_offsets[i];
|
|
101
|
+
MD_OFFSET end = attr.substr_offsets[i + 1];
|
|
102
|
+
const char* sub = attr.text + off;
|
|
103
|
+
size_t len = end - off;
|
|
104
|
+
switch (attr.substr_types[i]) {
|
|
105
|
+
case MD_TEXT_ENTITY:
|
|
106
|
+
appendEntity(out, sub, len);
|
|
107
|
+
break;
|
|
108
|
+
case MD_TEXT_NULLCHAR:
|
|
109
|
+
out.append("�");
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
out.append(sub, len);
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Nesting cap: malicious documents (">"×20000) otherwise build ASTs deep
|
|
120
|
+
// enough to overflow the stack in every recursive consumer. Content beyond
|
|
121
|
+
// the cap flattens into the deepest allowed node.
|
|
122
|
+
constexpr size_t kMaxNestingDepth = 64;
|
|
123
|
+
|
|
124
|
+
struct ParseState {
|
|
125
|
+
MarkdownDocument* doc = nullptr;
|
|
126
|
+
std::vector<Node*> stack;
|
|
127
|
+
// Enter callbacks swallowed by the depth cap; the matching leave
|
|
128
|
+
// callbacks must not pop real nodes.
|
|
129
|
+
int overflowDepth = 0;
|
|
130
|
+
// Inside an image span all inline callbacks accumulate into the alt text.
|
|
131
|
+
Node* imageNode = nullptr;
|
|
132
|
+
int imageSpanDepth = 0;
|
|
133
|
+
bool inHeaderRow = false;
|
|
134
|
+
|
|
135
|
+
Node* top() {
|
|
136
|
+
return stack.back();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
bool atDepthCap() const {
|
|
140
|
+
return stack.size() >= kMaxNestingDepth;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
Node* push(NodeType type) {
|
|
144
|
+
Node* node = doc->arena.alloc(type);
|
|
145
|
+
if (!stack.empty()) {
|
|
146
|
+
stack.back()->children.push_back(node);
|
|
147
|
+
}
|
|
148
|
+
stack.push_back(node);
|
|
149
|
+
return node;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
void pop() {
|
|
153
|
+
stack.pop_back();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
void appendText(const char* s, size_t n) {
|
|
157
|
+
Node* parent = top();
|
|
158
|
+
if (parent->type == NodeType::CodeBlock || parent->type == NodeType::InlineCode) {
|
|
159
|
+
parent->text.append(s, n);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (!parent->children.empty() &&
|
|
163
|
+
parent->children.back()->type == NodeType::Text &&
|
|
164
|
+
!parent->children.back()->verbatim) {
|
|
165
|
+
parent->children.back()->text.append(s, n);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
Node* text = doc->arena.alloc(NodeType::Text);
|
|
169
|
+
text->text.assign(s, n);
|
|
170
|
+
parent->children.push_back(text);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Entity-derived text: the characters were explicitly de-fanged by the
|
|
174
|
+
// author, so the node is tagged verbatim and kept unmerged — the
|
|
175
|
+
// inline-extension scanner skips it.
|
|
176
|
+
void appendVerbatimText(const std::string& s) {
|
|
177
|
+
Node* parent = top();
|
|
178
|
+
if (parent->type == NodeType::CodeBlock || parent->type == NodeType::InlineCode) {
|
|
179
|
+
parent->text.append(s);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
Node* text = doc->arena.alloc(NodeType::Text);
|
|
183
|
+
text->text = s;
|
|
184
|
+
text->verbatim = true;
|
|
185
|
+
parent->children.push_back(text);
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
int onEnterBlock(MD_BLOCKTYPE type, void* detail, void* userdata) {
|
|
190
|
+
auto* state = static_cast<ParseState*>(userdata);
|
|
191
|
+
// Depth cap: swallow pushes beyond the limit (THEAD/TBODY/HR/HTML never
|
|
192
|
+
// push, and their leaves never pop, so they bypass the accounting).
|
|
193
|
+
if (type != MD_BLOCK_THEAD && type != MD_BLOCK_TBODY &&
|
|
194
|
+
type != MD_BLOCK_HR && type != MD_BLOCK_HTML && state->atDepthCap()) {
|
|
195
|
+
state->overflowDepth++;
|
|
196
|
+
return 0;
|
|
197
|
+
}
|
|
198
|
+
switch (type) {
|
|
199
|
+
case MD_BLOCK_DOC:
|
|
200
|
+
state->doc->root = state->push(NodeType::Document);
|
|
201
|
+
break;
|
|
202
|
+
case MD_BLOCK_QUOTE:
|
|
203
|
+
state->push(NodeType::BlockQuote);
|
|
204
|
+
break;
|
|
205
|
+
case MD_BLOCK_UL: {
|
|
206
|
+
Node* node = state->push(NodeType::List);
|
|
207
|
+
node->ordered = false;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
case MD_BLOCK_OL: {
|
|
211
|
+
auto* d = static_cast<MD_BLOCK_OL_DETAIL*>(detail);
|
|
212
|
+
Node* node = state->push(NodeType::List);
|
|
213
|
+
node->ordered = true;
|
|
214
|
+
node->startIndex = static_cast<int32_t>(d->start);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
case MD_BLOCK_LI:
|
|
218
|
+
state->push(NodeType::ListItem);
|
|
219
|
+
break;
|
|
220
|
+
case MD_BLOCK_HR:
|
|
221
|
+
state->push(NodeType::ThematicBreak);
|
|
222
|
+
state->pop();
|
|
223
|
+
break;
|
|
224
|
+
case MD_BLOCK_H: {
|
|
225
|
+
auto* d = static_cast<MD_BLOCK_H_DETAIL*>(detail);
|
|
226
|
+
Node* node = state->push(NodeType::Heading);
|
|
227
|
+
node->level = static_cast<uint8_t>(d->level);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
case MD_BLOCK_CODE: {
|
|
231
|
+
auto* d = static_cast<MD_BLOCK_CODE_DETAIL*>(detail);
|
|
232
|
+
Node* node = state->push(NodeType::CodeBlock);
|
|
233
|
+
node->url = attributeToString(d->lang);
|
|
234
|
+
break;
|
|
235
|
+
}
|
|
236
|
+
case MD_BLOCK_P:
|
|
237
|
+
state->push(NodeType::Paragraph);
|
|
238
|
+
break;
|
|
239
|
+
case MD_BLOCK_TABLE:
|
|
240
|
+
state->push(NodeType::Table);
|
|
241
|
+
break;
|
|
242
|
+
case MD_BLOCK_THEAD:
|
|
243
|
+
state->inHeaderRow = true;
|
|
244
|
+
break;
|
|
245
|
+
case MD_BLOCK_TBODY:
|
|
246
|
+
state->inHeaderRow = false;
|
|
247
|
+
break;
|
|
248
|
+
case MD_BLOCK_TR: {
|
|
249
|
+
Node* node = state->push(NodeType::TableRow);
|
|
250
|
+
node->level = state->inHeaderRow ? 1 : 0;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
case MD_BLOCK_TH:
|
|
254
|
+
case MD_BLOCK_TD: {
|
|
255
|
+
auto* d = static_cast<MD_BLOCK_TD_DETAIL*>(detail);
|
|
256
|
+
Node* node = state->push(NodeType::TableCell);
|
|
257
|
+
switch (d->align) {
|
|
258
|
+
case MD_ALIGN_LEFT:
|
|
259
|
+
node->level = static_cast<uint8_t>(CellAlign::Left);
|
|
260
|
+
break;
|
|
261
|
+
case MD_ALIGN_CENTER:
|
|
262
|
+
node->level = static_cast<uint8_t>(CellAlign::Center);
|
|
263
|
+
break;
|
|
264
|
+
case MD_ALIGN_RIGHT:
|
|
265
|
+
node->level = static_cast<uint8_t>(CellAlign::Right);
|
|
266
|
+
break;
|
|
267
|
+
default:
|
|
268
|
+
node->level = static_cast<uint8_t>(CellAlign::Default);
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
case MD_BLOCK_HTML:
|
|
274
|
+
// MD_FLAG_NOHTML is set; not reached.
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
return 0;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
int onLeaveBlock(MD_BLOCKTYPE type, void* detail, void* userdata) {
|
|
281
|
+
(void)detail;
|
|
282
|
+
auto* state = static_cast<ParseState*>(userdata);
|
|
283
|
+
switch (type) {
|
|
284
|
+
case MD_BLOCK_THEAD:
|
|
285
|
+
case MD_BLOCK_TBODY:
|
|
286
|
+
case MD_BLOCK_HR:
|
|
287
|
+
case MD_BLOCK_HTML:
|
|
288
|
+
break;
|
|
289
|
+
default:
|
|
290
|
+
if (state->overflowDepth > 0) {
|
|
291
|
+
state->overflowDepth--;
|
|
292
|
+
} else {
|
|
293
|
+
state->pop();
|
|
294
|
+
}
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
return 0;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
int onEnterSpan(MD_SPANTYPE type, void* detail, void* userdata) {
|
|
301
|
+
auto* state = static_cast<ParseState*>(userdata);
|
|
302
|
+
if (state->imageNode != nullptr) {
|
|
303
|
+
// Styled spans inside image alt text are flattened.
|
|
304
|
+
state->imageSpanDepth++;
|
|
305
|
+
return 0;
|
|
306
|
+
}
|
|
307
|
+
if (state->atDepthCap()) {
|
|
308
|
+
state->overflowDepth++;
|
|
309
|
+
return 0;
|
|
310
|
+
}
|
|
311
|
+
switch (type) {
|
|
312
|
+
case MD_SPAN_EM:
|
|
313
|
+
state->push(NodeType::Italic);
|
|
314
|
+
break;
|
|
315
|
+
case MD_SPAN_STRONG:
|
|
316
|
+
state->push(NodeType::Bold);
|
|
317
|
+
break;
|
|
318
|
+
case MD_SPAN_A: {
|
|
319
|
+
auto* d = static_cast<MD_SPAN_A_DETAIL*>(detail);
|
|
320
|
+
Node* node = state->push(NodeType::Link);
|
|
321
|
+
node->url = attributeToString(d->href);
|
|
322
|
+
break;
|
|
323
|
+
}
|
|
324
|
+
case MD_SPAN_IMG: {
|
|
325
|
+
auto* d = static_cast<MD_SPAN_IMG_DETAIL*>(detail);
|
|
326
|
+
Node* node = state->doc->arena.alloc(NodeType::Image);
|
|
327
|
+
node->url = attributeToString(d->src);
|
|
328
|
+
state->top()->children.push_back(node);
|
|
329
|
+
state->imageNode = node;
|
|
330
|
+
break;
|
|
331
|
+
}
|
|
332
|
+
case MD_SPAN_CODE:
|
|
333
|
+
state->push(NodeType::InlineCode);
|
|
334
|
+
break;
|
|
335
|
+
case MD_SPAN_DEL:
|
|
336
|
+
// MD_FLAG_STRIKETHROUGH is off; "~~" is handled by InlineExtensions.
|
|
337
|
+
state->push(NodeType::Strikethrough);
|
|
338
|
+
break;
|
|
339
|
+
default:
|
|
340
|
+
// Unsupported span types (latex, wikilink, underline) are not enabled.
|
|
341
|
+
state->push(NodeType::Paragraph);
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
return 0;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
int onLeaveSpan(MD_SPANTYPE type, void* detail, void* userdata) {
|
|
348
|
+
(void)detail;
|
|
349
|
+
auto* state = static_cast<ParseState*>(userdata);
|
|
350
|
+
if (state->imageNode != nullptr) {
|
|
351
|
+
if (state->imageSpanDepth > 0) {
|
|
352
|
+
// Spans nested inside alt text unwind here — including nested images,
|
|
353
|
+
// whose leave must not clear the OUTER image's accumulation.
|
|
354
|
+
state->imageSpanDepth--;
|
|
355
|
+
return 0;
|
|
356
|
+
}
|
|
357
|
+
if (type == MD_SPAN_IMG) {
|
|
358
|
+
state->imageNode = nullptr;
|
|
359
|
+
return 0;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
if (state->overflowDepth > 0) {
|
|
363
|
+
state->overflowDepth--;
|
|
364
|
+
return 0;
|
|
365
|
+
}
|
|
366
|
+
state->pop();
|
|
367
|
+
return 0;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
int onText(MD_TEXTTYPE type, const MD_CHAR* text, MD_SIZE size, void* userdata) {
|
|
371
|
+
auto* state = static_cast<ParseState*>(userdata);
|
|
372
|
+
|
|
373
|
+
if (state->imageNode != nullptr) {
|
|
374
|
+
switch (type) {
|
|
375
|
+
case MD_TEXT_ENTITY:
|
|
376
|
+
appendEntity(state->imageNode->text, text, size);
|
|
377
|
+
break;
|
|
378
|
+
case MD_TEXT_NULLCHAR:
|
|
379
|
+
state->imageNode->text.append("�");
|
|
380
|
+
break;
|
|
381
|
+
case MD_TEXT_BR:
|
|
382
|
+
case MD_TEXT_SOFTBR:
|
|
383
|
+
state->imageNode->text.push_back(' ');
|
|
384
|
+
break;
|
|
385
|
+
default:
|
|
386
|
+
state->imageNode->text.append(text, size);
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
return 0;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
switch (type) {
|
|
393
|
+
case MD_TEXT_NORMAL:
|
|
394
|
+
case MD_TEXT_CODE:
|
|
395
|
+
case MD_TEXT_HTML:
|
|
396
|
+
state->appendText(text, size);
|
|
397
|
+
break;
|
|
398
|
+
case MD_TEXT_ENTITY: {
|
|
399
|
+
std::string translated;
|
|
400
|
+
appendEntity(translated, text, size);
|
|
401
|
+
state->appendVerbatimText(translated);
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
case MD_TEXT_NULLCHAR: {
|
|
405
|
+
static const char* kReplacement = "�";
|
|
406
|
+
state->appendText(kReplacement, 3);
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
case MD_TEXT_BR: {
|
|
410
|
+
Node* parent = state->top();
|
|
411
|
+
if (parent->type == NodeType::CodeBlock) {
|
|
412
|
+
parent->text.push_back('\n');
|
|
413
|
+
} else {
|
|
414
|
+
parent->children.push_back(state->doc->arena.alloc(NodeType::HardBreak));
|
|
415
|
+
}
|
|
416
|
+
break;
|
|
417
|
+
}
|
|
418
|
+
case MD_TEXT_SOFTBR: {
|
|
419
|
+
Node* parent = state->top();
|
|
420
|
+
if (parent->type == NodeType::CodeBlock) {
|
|
421
|
+
parent->text.push_back('\n');
|
|
422
|
+
} else {
|
|
423
|
+
parent->children.push_back(state->doc->arena.alloc(NodeType::SoftBreak));
|
|
424
|
+
}
|
|
425
|
+
break;
|
|
426
|
+
}
|
|
427
|
+
case MD_TEXT_LATEXMATH:
|
|
428
|
+
state->appendText(text, size);
|
|
429
|
+
break;
|
|
430
|
+
}
|
|
431
|
+
return 0;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
} // namespace
|
|
435
|
+
|
|
436
|
+
std::unique_ptr<MarkdownDocument> parseMarkdown(const std::string& markdown) {
|
|
437
|
+
auto doc = std::make_unique<MarkdownDocument>();
|
|
438
|
+
|
|
439
|
+
const std::string preprocessed = preprocessMarkdown(markdown);
|
|
440
|
+
|
|
441
|
+
ParseState state;
|
|
442
|
+
state.doc = doc.get();
|
|
443
|
+
|
|
444
|
+
MD_PARSER parser = {};
|
|
445
|
+
parser.abi_version = 0;
|
|
446
|
+
parser.flags = MD_FLAG_TABLES | MD_FLAG_PERMISSIVEAUTOLINKS | MD_FLAG_NOHTML;
|
|
447
|
+
parser.enter_block = onEnterBlock;
|
|
448
|
+
parser.leave_block = onLeaveBlock;
|
|
449
|
+
parser.enter_span = onEnterSpan;
|
|
450
|
+
parser.leave_span = onLeaveSpan;
|
|
451
|
+
parser.text = onText;
|
|
452
|
+
|
|
453
|
+
const int result = md_parse(
|
|
454
|
+
preprocessed.data(), static_cast<MD_SIZE>(preprocessed.size()), &parser, &state);
|
|
455
|
+
|
|
456
|
+
if (result != 0 || doc->root == nullptr) {
|
|
457
|
+
doc->root = doc->arena.alloc(NodeType::Document);
|
|
458
|
+
Node* paragraph = doc->arena.alloc(NodeType::Paragraph);
|
|
459
|
+
Node* text = doc->arena.alloc(NodeType::Text);
|
|
460
|
+
text->text = markdown;
|
|
461
|
+
paragraph->children.push_back(text);
|
|
462
|
+
doc->root->children.push_back(paragraph);
|
|
463
|
+
return doc;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
applyInlineExtensions(*doc);
|
|
467
|
+
return doc;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
} // namespace jetmarkdown
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <memory>
|
|
4
|
+
#include <string>
|
|
5
|
+
|
|
6
|
+
#include "Ast.h"
|
|
7
|
+
|
|
8
|
+
namespace jetmarkdown {
|
|
9
|
+
|
|
10
|
+
// Parses markdown into an AST: preprocess (spoiler escaping) -> md4c ->
|
|
11
|
+
// inline extension scan (spoilers, strikethrough, superscript, subscript).
|
|
12
|
+
// Never returns nullptr; on parser failure the document contains a single
|
|
13
|
+
// paragraph with the raw input as plain text.
|
|
14
|
+
std::unique_ptr<MarkdownDocument> parseMarkdown(const std::string& markdown);
|
|
15
|
+
|
|
16
|
+
} // namespace jetmarkdown
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#include "Preprocess.h"
|
|
2
|
+
|
|
3
|
+
namespace jetmarkdown {
|
|
4
|
+
|
|
5
|
+
namespace {
|
|
6
|
+
|
|
7
|
+
bool isFenceLine(const std::string& s, size_t lineStart, size_t lineEnd) {
|
|
8
|
+
size_t i = lineStart;
|
|
9
|
+
size_t spaces = 0;
|
|
10
|
+
while (i < lineEnd && s[i] == ' ' && spaces < 3) {
|
|
11
|
+
i++;
|
|
12
|
+
spaces++;
|
|
13
|
+
}
|
|
14
|
+
if (i >= lineEnd) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
char fence = s[i];
|
|
18
|
+
if (fence != '`' && fence != '~') {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
size_t run = 0;
|
|
22
|
+
while (i < lineEnd && s[i] == fence) {
|
|
23
|
+
i++;
|
|
24
|
+
run++;
|
|
25
|
+
}
|
|
26
|
+
return run >= 3;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// md4c strips backslashes before the inline-extension scanner runs, so an
|
|
30
|
+
// escaped extension delimiter (\| \^ \~) would be re-assembled and re-trigger
|
|
31
|
+
// spoilers/sup/sub. Rewrite them to entity forms here, where the backslash is
|
|
32
|
+
// still visible; the parser materializes entities as verbatim text the
|
|
33
|
+
// scanner skips. Backtick spans are honored (their backslashes are literal
|
|
34
|
+
// code content), approximated per line.
|
|
35
|
+
void appendWithEscapeRewrite(
|
|
36
|
+
const std::string& in, size_t start, size_t end, std::string& out) {
|
|
37
|
+
size_t i = start;
|
|
38
|
+
while (i < end) {
|
|
39
|
+
const char c = in[i];
|
|
40
|
+
if (c == '`') {
|
|
41
|
+
// Enter a code span: find the closing run of the same length and copy
|
|
42
|
+
// the whole span untouched. No closer -> literal backticks.
|
|
43
|
+
size_t runLen = 0;
|
|
44
|
+
size_t j = i;
|
|
45
|
+
while (j < end && in[j] == '`') {
|
|
46
|
+
j++;
|
|
47
|
+
runLen++;
|
|
48
|
+
}
|
|
49
|
+
size_t close = std::string::npos;
|
|
50
|
+
size_t scan = j;
|
|
51
|
+
while (scan < end) {
|
|
52
|
+
if (in[scan] != '`') {
|
|
53
|
+
scan++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
size_t closeLen = 0;
|
|
57
|
+
size_t r = scan;
|
|
58
|
+
while (r < end && in[r] == '`') {
|
|
59
|
+
r++;
|
|
60
|
+
closeLen++;
|
|
61
|
+
}
|
|
62
|
+
if (closeLen == runLen) {
|
|
63
|
+
close = scan;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
scan = r;
|
|
67
|
+
}
|
|
68
|
+
if (close != std::string::npos) {
|
|
69
|
+
out.append(in, i, (close + runLen) - i);
|
|
70
|
+
i = close + runLen;
|
|
71
|
+
} else {
|
|
72
|
+
out.append(in, i, j - i);
|
|
73
|
+
i = j;
|
|
74
|
+
}
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (c == '\\' && i + 1 < end) {
|
|
78
|
+
const char next = in[i + 1];
|
|
79
|
+
switch (next) {
|
|
80
|
+
case '|':
|
|
81
|
+
out.append("|");
|
|
82
|
+
break;
|
|
83
|
+
case '^':
|
|
84
|
+
out.append("^");
|
|
85
|
+
break;
|
|
86
|
+
case '~':
|
|
87
|
+
out.append("~");
|
|
88
|
+
break;
|
|
89
|
+
// The spoiler pair tokens ">!" / "!<" are also assembled from
|
|
90
|
+
// adjacent characters after md4c unescapes, so their pieces get the
|
|
91
|
+
// same treatment.
|
|
92
|
+
case '!':
|
|
93
|
+
out.append("!");
|
|
94
|
+
break;
|
|
95
|
+
case '<':
|
|
96
|
+
out.append("<");
|
|
97
|
+
break;
|
|
98
|
+
case '>':
|
|
99
|
+
out.append(">");
|
|
100
|
+
break;
|
|
101
|
+
default:
|
|
102
|
+
// Copy escape pairs atomically so "\\|" reads as literal
|
|
103
|
+
// backslash + active pipe.
|
|
104
|
+
out.push_back(c);
|
|
105
|
+
out.push_back(next);
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
i += 2;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
out.push_back(c);
|
|
112
|
+
i++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
} // namespace
|
|
117
|
+
|
|
118
|
+
std::string preprocessMarkdown(const std::string& input) {
|
|
119
|
+
std::string out;
|
|
120
|
+
out.reserve(input.size() + 8);
|
|
121
|
+
|
|
122
|
+
bool inFence = false;
|
|
123
|
+
size_t pos = 0;
|
|
124
|
+
const size_t n = input.size();
|
|
125
|
+
|
|
126
|
+
while (pos < n) {
|
|
127
|
+
size_t lineEnd = input.find('\n', pos);
|
|
128
|
+
if (lineEnd == std::string::npos) {
|
|
129
|
+
lineEnd = n;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (isFenceLine(input, pos, lineEnd)) {
|
|
133
|
+
inFence = !inFence;
|
|
134
|
+
out.append(input, pos, lineEnd - pos);
|
|
135
|
+
} else if (inFence) {
|
|
136
|
+
out.append(input, pos, lineEnd - pos);
|
|
137
|
+
} else {
|
|
138
|
+
// Skip up to 3 leading spaces; 4+ may be indented code -- leave alone.
|
|
139
|
+
size_t i = pos;
|
|
140
|
+
size_t spaces = 0;
|
|
141
|
+
while (i < lineEnd && (input[i] == ' ' || input[i] == '\t') && spaces < 4) {
|
|
142
|
+
if (input[i] == '\t') {
|
|
143
|
+
spaces = 4;
|
|
144
|
+
} else {
|
|
145
|
+
spaces++;
|
|
146
|
+
}
|
|
147
|
+
i++;
|
|
148
|
+
}
|
|
149
|
+
bool escaped = false;
|
|
150
|
+
if (spaces < 4) {
|
|
151
|
+
// Walk any blockquote markers; escape the ">" that opens ">!".
|
|
152
|
+
size_t j = i;
|
|
153
|
+
while (j < lineEnd && input[j] == '>') {
|
|
154
|
+
if (j + 1 < lineEnd && input[j + 1] == '!') {
|
|
155
|
+
out.append(input, pos, j - pos);
|
|
156
|
+
out.push_back('\\');
|
|
157
|
+
appendWithEscapeRewrite(input, j, lineEnd, out);
|
|
158
|
+
escaped = true;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
j++;
|
|
162
|
+
if (j < lineEnd && input[j] == ' ') {
|
|
163
|
+
j++;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (!escaped) {
|
|
168
|
+
appendWithEscapeRewrite(input, pos, lineEnd, out);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (lineEnd < n) {
|
|
173
|
+
out.push_back('\n');
|
|
174
|
+
}
|
|
175
|
+
pos = lineEnd + 1;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
} // namespace jetmarkdown
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#include <string>
|
|
4
|
+
|
|
5
|
+
namespace jetmarkdown {
|
|
6
|
+
|
|
7
|
+
// Escapes a line-leading ">" when it opens a Reddit spoiler (">!") so md4c
|
|
8
|
+
// does not consume it as a blockquote marker. Runs before md4c; the inline
|
|
9
|
+
// extension scanner then recognizes ">!...!<" as a spoiler in plain text.
|
|
10
|
+
// Skips fenced code blocks and lines indented 4+ spaces (potential indented
|
|
11
|
+
// code). All other spoiler handling happens post-parse.
|
|
12
|
+
std::string preprocessMarkdown(const std::string& input);
|
|
13
|
+
|
|
14
|
+
} // namespace jetmarkdown
|
package/cpp/md4c/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
|
|
2
|
+
# The MIT License (MIT)
|
|
3
|
+
|
|
4
|
+
Copyright © 2016-2024 Martin Mitáš
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a
|
|
7
|
+
copy of this software and associated documentation files (the “Software”),
|
|
8
|
+
to deal in the Software without restriction, including without limitation
|
|
9
|
+
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
10
|
+
and/or sell copies of the Software, and to permit persons to whom the
|
|
11
|
+
Software is furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included
|
|
14
|
+
in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
17
|
+
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
19
|
+
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
21
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
|
|
22
|
+
IN THE SOFTWARE.
|
package/cpp/md4c/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
release-0.5.3
|