react-native-enriched-markdown 0.1.0 → 0.2.0
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/LICENSE +20 -0
- package/README.md +551 -0
- package/ReactNativeEnrichedMarkdown.podspec +27 -0
- package/android/build.gradle +101 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +54 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +26 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ComponentDescriptors.cpp +22 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ComponentDescriptors.h +24 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.cpp +33 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +31 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +82 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +1388 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ShadowNodes.cpp +17 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/ShadowNodes.h +32 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/States.cpp +16 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/States.h +20 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/baseline-prof.txt +65 -0
- package/android/src/main/cpp/jni-adapter.cpp +220 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +270 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +15 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +173 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextPackage.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/MeasurementStore.kt +385 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/accessibility/MarkdownAccessibilityHelper.kt +279 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkLongPressEvent.kt +23 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/events/LinkPressEvent.kt +23 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/MarkdownASTNode.kt +31 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +62 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockStyleContext.kt +166 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/BlockquoteRenderer.kt +84 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +104 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +36 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt +16 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +70 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +68 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +16 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +29 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListContextManager.kt +105 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListItemRenderer.kt +59 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +76 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +103 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +80 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +109 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +86 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrikethroughRenderer.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +30 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +45 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/UnderlineRenderer.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/BaseListSpan.kt +136 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/BlockquoteSpan.kt +135 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBackgroundSpan.kt +180 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeBlockSpan.kt +196 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/CodeSpan.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/EmphasisSpan.kt +34 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/HeadingSpan.kt +38 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/ImageSpan.kt +321 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +27 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +51 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/MarginBottomSpan.kt +76 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/OrderedListSpan.kt +87 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrikethroughSpan.kt +12 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/StrongSpan.kt +37 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/TextSpan.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/ThematicBreakSpan.kt +69 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/UnorderedListSpan.kt +69 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BaseBlockStyle.kt +11 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +51 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +54 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeStyle.kt +21 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/EmphasisStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/HeadingStyle.kt +33 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +23 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/InlineImageStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/LinkStyle.kt +19 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ListStyle.kt +57 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ParagraphStyle.kt +33 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrikethroughStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StrongStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleConfig.kt +211 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleParser.kt +92 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/TextAlignment.kt +32 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ThematicBreakStyle.kt +23 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/UnderlineStyle.kt +17 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/AsyncDrawable.kt +91 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/HTMLGenerator.kt +827 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/LinkLongPressMovementMethod.kt +121 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/MarkdownExtractor.kt +375 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/SelectionActionMode.kt +139 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/Utils.kt +183 -0
- package/android/src/main/jni/CMakeLists.txt +70 -0
- package/android/src/main/jni/EnrichedMarkdownTextSpec.cpp +21 -0
- package/android/src/main/jni/EnrichedMarkdownTextSpec.h +25 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextComponentDescriptor.h +29 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextMeasurementManager.cpp +45 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextMeasurementManager.h +21 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.cpp +20 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +37 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +22 -0
- package/cpp/md4c/md4c.c +6492 -0
- package/cpp/md4c/md4c.h +402 -0
- package/cpp/parser/MD4CParser.cpp +327 -0
- package/cpp/parser/MD4CParser.hpp +27 -0
- package/cpp/parser/MarkdownASTNode.hpp +51 -0
- package/ios/EnrichedMarkdownText.h +18 -0
- package/ios/EnrichedMarkdownText.mm +1401 -0
- package/ios/attachments/EnrichedMarkdownImageAttachment.h +23 -0
- package/ios/attachments/EnrichedMarkdownImageAttachment.m +185 -0
- package/ios/attachments/ThematicBreakAttachment.h +15 -0
- package/ios/attachments/ThematicBreakAttachment.m +33 -0
- package/ios/generated/EnrichedMarkdownTextSpec/ComponentDescriptors.cpp +22 -0
- package/ios/generated/EnrichedMarkdownTextSpec/ComponentDescriptors.h +24 -0
- package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.cpp +33 -0
- package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.h +31 -0
- package/ios/generated/EnrichedMarkdownTextSpec/Props.cpp +82 -0
- package/ios/generated/EnrichedMarkdownTextSpec/Props.h +1388 -0
- package/ios/generated/EnrichedMarkdownTextSpec/RCTComponentViewHelpers.h +20 -0
- package/ios/generated/EnrichedMarkdownTextSpec/ShadowNodes.cpp +17 -0
- package/ios/generated/EnrichedMarkdownTextSpec/ShadowNodes.h +32 -0
- package/ios/generated/EnrichedMarkdownTextSpec/States.cpp +16 -0
- package/ios/generated/EnrichedMarkdownTextSpec/States.h +20 -0
- package/ios/internals/EnrichedMarkdownTextComponentDescriptor.h +19 -0
- package/ios/internals/EnrichedMarkdownTextShadowNode.h +43 -0
- package/ios/internals/EnrichedMarkdownTextShadowNode.mm +85 -0
- package/ios/internals/EnrichedMarkdownTextState.h +24 -0
- package/ios/parser/MarkdownASTNode.h +35 -0
- package/ios/parser/MarkdownASTNode.m +32 -0
- package/ios/parser/MarkdownParser.h +17 -0
- package/ios/parser/MarkdownParser.mm +42 -0
- package/ios/parser/MarkdownParserBridge.mm +120 -0
- package/ios/renderer/AttributedRenderer.h +11 -0
- package/ios/renderer/AttributedRenderer.m +152 -0
- package/ios/renderer/BlockquoteRenderer.h +7 -0
- package/ios/renderer/BlockquoteRenderer.m +160 -0
- package/ios/renderer/CodeBlockRenderer.h +10 -0
- package/ios/renderer/CodeBlockRenderer.m +90 -0
- package/ios/renderer/CodeRenderer.h +10 -0
- package/ios/renderer/CodeRenderer.m +60 -0
- package/ios/renderer/EmphasisRenderer.h +6 -0
- package/ios/renderer/EmphasisRenderer.m +96 -0
- package/ios/renderer/HeadingRenderer.h +7 -0
- package/ios/renderer/HeadingRenderer.m +105 -0
- package/ios/renderer/ImageRenderer.h +12 -0
- package/ios/renderer/ImageRenderer.m +83 -0
- package/ios/renderer/LinkRenderer.h +7 -0
- package/ios/renderer/LinkRenderer.m +69 -0
- package/ios/renderer/ListItemRenderer.h +16 -0
- package/ios/renderer/ListItemRenderer.m +103 -0
- package/ios/renderer/ListRenderer.h +13 -0
- package/ios/renderer/ListRenderer.m +70 -0
- package/ios/renderer/NodeRenderer.h +8 -0
- package/ios/renderer/ParagraphRenderer.h +7 -0
- package/ios/renderer/ParagraphRenderer.m +80 -0
- package/ios/renderer/RenderContext.h +105 -0
- package/ios/renderer/RenderContext.m +312 -0
- package/ios/renderer/RendererFactory.h +12 -0
- package/ios/renderer/RendererFactory.m +116 -0
- package/ios/renderer/StrikethroughRenderer.h +6 -0
- package/ios/renderer/StrikethroughRenderer.m +40 -0
- package/ios/renderer/StrongRenderer.h +6 -0
- package/ios/renderer/StrongRenderer.m +83 -0
- package/ios/renderer/TextRenderer.h +6 -0
- package/ios/renderer/TextRenderer.m +16 -0
- package/ios/renderer/ThematicBreakRenderer.h +5 -0
- package/ios/renderer/ThematicBreakRenderer.m +53 -0
- package/ios/renderer/UnderlineRenderer.h +6 -0
- package/ios/renderer/UnderlineRenderer.m +39 -0
- package/ios/styles/StyleConfig.h +274 -0
- package/ios/styles/StyleConfig.mm +1806 -0
- package/ios/utils/AccessibilityInfo.h +35 -0
- package/ios/utils/AccessibilityInfo.m +24 -0
- package/ios/utils/BlockquoteBorder.h +20 -0
- package/ios/utils/BlockquoteBorder.m +92 -0
- package/ios/utils/CodeBackground.h +19 -0
- package/ios/utils/CodeBackground.m +191 -0
- package/ios/utils/CodeBlockBackground.h +17 -0
- package/ios/utils/CodeBlockBackground.m +82 -0
- package/ios/utils/EditMenuUtils.h +22 -0
- package/ios/utils/EditMenuUtils.m +118 -0
- package/ios/utils/FontUtils.h +25 -0
- package/ios/utils/FontUtils.m +27 -0
- package/ios/utils/HTMLGenerator.h +20 -0
- package/ios/utils/HTMLGenerator.m +793 -0
- package/ios/utils/LastElementUtils.h +53 -0
- package/ios/utils/ListMarkerDrawer.h +15 -0
- package/ios/utils/ListMarkerDrawer.m +127 -0
- package/ios/utils/MarkdownAccessibilityElementBuilder.h +45 -0
- package/ios/utils/MarkdownAccessibilityElementBuilder.m +323 -0
- package/ios/utils/MarkdownExtractor.h +17 -0
- package/ios/utils/MarkdownExtractor.m +308 -0
- package/ios/utils/ParagraphStyleUtils.h +21 -0
- package/ios/utils/ParagraphStyleUtils.m +111 -0
- package/ios/utils/PasteboardUtils.h +36 -0
- package/ios/utils/PasteboardUtils.m +134 -0
- package/ios/utils/RTFExportUtils.h +24 -0
- package/ios/utils/RTFExportUtils.m +297 -0
- package/ios/utils/RuntimeKeys.h +38 -0
- package/ios/utils/RuntimeKeys.m +11 -0
- package/ios/utils/TextViewLayoutManager.h +14 -0
- package/ios/utils/TextViewLayoutManager.mm +113 -0
- package/lib/module/EnrichedMarkdownText.js +65 -0
- package/lib/module/EnrichedMarkdownText.js.map +1 -0
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +210 -0
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/normalizeMarkdownStyle.js +384 -0
- package/lib/module/normalizeMarkdownStyle.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/EnrichedMarkdownText.d.ts +183 -0
- package/lib/typescript/src/EnrichedMarkdownText.d.ts.map +1 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +185 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/normalizeMarkdownStyle.d.ts +6 -0
- package/lib/typescript/src/normalizeMarkdownStyle.d.ts.map +1 -0
- package/package.json +186 -1
- package/react-native.config.js +13 -0
- package/src/EnrichedMarkdownText.tsx +280 -0
- package/src/EnrichedMarkdownTextNativeComponent.ts +210 -0
- package/src/index.tsx +10 -0
- package/src/normalizeMarkdownStyle.ts +423 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
#import "MarkdownExtractor.h"
|
|
2
|
+
#import "BlockquoteBorder.h"
|
|
3
|
+
#import "EnrichedMarkdownImageAttachment.h"
|
|
4
|
+
#import "LastElementUtils.h"
|
|
5
|
+
#import "RuntimeKeys.h"
|
|
6
|
+
#import "ThematicBreakAttachment.h"
|
|
7
|
+
|
|
8
|
+
#pragma mark - Extraction Context
|
|
9
|
+
|
|
10
|
+
typedef struct {
|
|
11
|
+
NSInteger blockquoteDepth; // -1 = not in blockquote
|
|
12
|
+
NSInteger listDepth; // -1 = not in list
|
|
13
|
+
BOOL needsBlankLine;
|
|
14
|
+
} ExtractionState;
|
|
15
|
+
|
|
16
|
+
#pragma mark - Helper Functions
|
|
17
|
+
|
|
18
|
+
static void ensureBlankLine(NSMutableString *result)
|
|
19
|
+
{
|
|
20
|
+
if (result.length == 0)
|
|
21
|
+
return;
|
|
22
|
+
if ([result hasSuffix:@"\n\n"])
|
|
23
|
+
return;
|
|
24
|
+
|
|
25
|
+
[result appendString:[result hasSuffix:@"\n"] ? @"\n" : @"\n\n"];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
static BOOL isAtLineStart(NSMutableString *result)
|
|
29
|
+
{
|
|
30
|
+
return result.length == 0 || [result hasSuffix:@"\n"];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Depth 0 = "> ", Depth 1 = "> > ", etc.
|
|
34
|
+
static NSString *buildBlockquotePrefix(NSInteger depth)
|
|
35
|
+
{
|
|
36
|
+
NSMutableString *prefix = [NSMutableString string];
|
|
37
|
+
for (NSInteger i = 0; i <= depth; i++) {
|
|
38
|
+
[prefix appendString:@"> "];
|
|
39
|
+
}
|
|
40
|
+
return prefix;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static NSString *buildListPrefix(NSInteger depth, BOOL isOrdered, NSInteger itemNumber)
|
|
44
|
+
{
|
|
45
|
+
NSString *indent = [@"" stringByPaddingToLength:(depth * 2) withString:@" " startingAtIndex:0];
|
|
46
|
+
NSString *marker = isOrdered ? [NSString stringWithFormat:@"%ld.", (long)itemNumber] : @"-";
|
|
47
|
+
return [NSString stringWithFormat:@"%@%@ ", indent, marker];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
static NSString *buildHeadingPrefix(NSInteger level)
|
|
51
|
+
{
|
|
52
|
+
return [NSString stringWithFormat:@"%@ ", [@"" stringByPaddingToLength:level withString:@"#" startingAtIndex:0]];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static void extractFontTraits(NSDictionary *attrs, BOOL *isBold, BOOL *isItalic, BOOL *isMonospace)
|
|
56
|
+
{
|
|
57
|
+
UIFont *font = attrs[NSFontAttributeName];
|
|
58
|
+
*isBold = NO;
|
|
59
|
+
*isItalic = NO;
|
|
60
|
+
*isMonospace = NO;
|
|
61
|
+
|
|
62
|
+
if (font) {
|
|
63
|
+
UIFontDescriptorSymbolicTraits traits = font.fontDescriptor.symbolicTraits;
|
|
64
|
+
*isBold = (traits & UIFontDescriptorTraitBold) != 0;
|
|
65
|
+
*isItalic = (traits & UIFontDescriptorTraitItalic) != 0;
|
|
66
|
+
*isMonospace = (traits & UIFontDescriptorTraitMonoSpace) != 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
static NSString *applyInlineFormatting(NSString *text, BOOL isBold, BOOL isItalic, BOOL isMonospace,
|
|
71
|
+
BOOL isStrikethrough, BOOL isUnderline, NSString *linkURL)
|
|
72
|
+
{
|
|
73
|
+
NSMutableString *result = [NSMutableString stringWithString:text];
|
|
74
|
+
|
|
75
|
+
// Innermost first
|
|
76
|
+
if (isMonospace && !linkURL) {
|
|
77
|
+
result = [NSMutableString stringWithFormat:@"`%@`", result];
|
|
78
|
+
}
|
|
79
|
+
if (isStrikethrough) {
|
|
80
|
+
result = [NSMutableString stringWithFormat:@"~~%@~~", result];
|
|
81
|
+
}
|
|
82
|
+
if (isUnderline && !linkURL) {
|
|
83
|
+
result = [NSMutableString stringWithFormat:@"<u>%@</u>", result];
|
|
84
|
+
}
|
|
85
|
+
if (isItalic) {
|
|
86
|
+
result = [NSMutableString stringWithFormat:@"*%@*", result];
|
|
87
|
+
}
|
|
88
|
+
if (isBold) {
|
|
89
|
+
result = [NSMutableString stringWithFormat:@"**%@**", result];
|
|
90
|
+
}
|
|
91
|
+
if (linkURL) {
|
|
92
|
+
result = [NSMutableString stringWithFormat:@"[%@](%@)", text, linkURL];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return result;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#pragma mark - Main Extraction Function
|
|
99
|
+
|
|
100
|
+
NSString *_Nullable extractMarkdownFromAttributedString(NSAttributedString *attributedText, NSRange range)
|
|
101
|
+
{
|
|
102
|
+
if (!attributedText || range.length == 0 || range.location >= attributedText.length) {
|
|
103
|
+
return nil;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
range.length = MIN(range.length, attributedText.length - range.location);
|
|
107
|
+
|
|
108
|
+
NSMutableString *result = [NSMutableString string];
|
|
109
|
+
|
|
110
|
+
// Headings may span multiple attribute runs
|
|
111
|
+
__block NSString *currentHeadingType = nil;
|
|
112
|
+
__block NSMutableString *headingContent = nil;
|
|
113
|
+
__block ExtractionState state = {.blockquoteDepth = -1, .listDepth = -1, .needsBlankLine = NO};
|
|
114
|
+
|
|
115
|
+
void (^flushHeading)(void) = ^{
|
|
116
|
+
if (!currentHeadingType || headingContent.length == 0)
|
|
117
|
+
return;
|
|
118
|
+
|
|
119
|
+
ensureBlankLine(result);
|
|
120
|
+
NSInteger level = [[currentHeadingType substringFromIndex:8] integerValue];
|
|
121
|
+
[result appendFormat:@"%@%@\n", buildHeadingPrefix(level), headingContent];
|
|
122
|
+
|
|
123
|
+
currentHeadingType = nil;
|
|
124
|
+
headingContent = nil;
|
|
125
|
+
state.needsBlankLine = YES;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
[attributedText
|
|
129
|
+
enumerateAttributesInRange:range
|
|
130
|
+
options:0
|
|
131
|
+
usingBlock:^(NSDictionary<NSAttributedStringKey, id> *attrs, NSRange attrRange, BOOL *stop) {
|
|
132
|
+
NSString *text = [[attributedText attributedSubstringFromRange:attrRange] string];
|
|
133
|
+
if (text.length == 0)
|
|
134
|
+
return;
|
|
135
|
+
|
|
136
|
+
// Images and Thematic Breaks
|
|
137
|
+
NSTextAttachment *attachment = attrs[NSAttachmentAttributeName];
|
|
138
|
+
if ([attachment isKindOfClass:[EnrichedMarkdownImageAttachment class]]) {
|
|
139
|
+
EnrichedMarkdownImageAttachment *img = (EnrichedMarkdownImageAttachment *)attachment;
|
|
140
|
+
if (!img.imageURL)
|
|
141
|
+
return;
|
|
142
|
+
|
|
143
|
+
if (img.isInline) {
|
|
144
|
+
[result appendFormat:@"", img.imageURL];
|
|
145
|
+
} else {
|
|
146
|
+
ensureBlankLine(result);
|
|
147
|
+
[result appendFormat:@"\n", img.imageURL];
|
|
148
|
+
state.needsBlankLine = YES;
|
|
149
|
+
state.blockquoteDepth = -1;
|
|
150
|
+
state.listDepth = -1;
|
|
151
|
+
}
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if ([attachment isKindOfClass:[ThematicBreakAttachment class]]) {
|
|
156
|
+
ensureBlankLine(result);
|
|
157
|
+
[result appendString:@"---\n"];
|
|
158
|
+
state.needsBlankLine = YES;
|
|
159
|
+
state.blockquoteDepth = -1;
|
|
160
|
+
state.listDepth = -1;
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if ([text isEqualToString:@"\uFFFC"])
|
|
165
|
+
return;
|
|
166
|
+
|
|
167
|
+
// Newlines
|
|
168
|
+
if ([text isEqualToString:@"\n"] || [text isEqualToString:@"\n\n"]) {
|
|
169
|
+
NSNumber *bqDepth = attrs[BlockquoteDepthAttributeName];
|
|
170
|
+
NSNumber *listDepth = attrs[@"ListDepth"];
|
|
171
|
+
BOOL inBlockquote = (bqDepth != nil);
|
|
172
|
+
BOOL inList = (listDepth != nil);
|
|
173
|
+
|
|
174
|
+
if (!inBlockquote && state.blockquoteDepth >= 0) {
|
|
175
|
+
ensureBlankLine(result);
|
|
176
|
+
state.blockquoteDepth = -1;
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!inList && state.listDepth >= 0) {
|
|
181
|
+
ensureBlankLine(result);
|
|
182
|
+
state.listDepth = -1;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (inBlockquote || inList) {
|
|
187
|
+
if (![result hasSuffix:@"\n"]) {
|
|
188
|
+
[result appendString:@"\n"];
|
|
189
|
+
}
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
ensureBlankLine(result);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Headings
|
|
198
|
+
NSString *markdownType = attrs[MarkdownTypeAttributeName];
|
|
199
|
+
|
|
200
|
+
if (markdownType && [markdownType hasPrefix:@"heading-"]) {
|
|
201
|
+
if (![markdownType isEqualToString:currentHeadingType]) {
|
|
202
|
+
flushHeading();
|
|
203
|
+
currentHeadingType = markdownType;
|
|
204
|
+
headingContent = [NSMutableString string];
|
|
205
|
+
}
|
|
206
|
+
[headingContent
|
|
207
|
+
appendString:[text stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]];
|
|
208
|
+
return;
|
|
209
|
+
} else if (currentHeadingType) {
|
|
210
|
+
flushHeading();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Code blocks
|
|
214
|
+
NSNumber *isCodeBlock = attrs[CodeBlockAttributeName];
|
|
215
|
+
if ([isCodeBlock boolValue]) {
|
|
216
|
+
if (state.needsBlankLine) {
|
|
217
|
+
ensureBlankLine(result);
|
|
218
|
+
state.needsBlankLine = NO;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
BOOL needsFence = (result.length == 0) || [result hasSuffix:@"\n\n"];
|
|
222
|
+
if (needsFence) {
|
|
223
|
+
[result appendString:@"```\n"];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
[result appendString:text];
|
|
227
|
+
|
|
228
|
+
if ([text hasSuffix:@"\n"]) {
|
|
229
|
+
[result appendString:@"```\n"];
|
|
230
|
+
state.needsBlankLine = YES;
|
|
231
|
+
}
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Blockquotes
|
|
236
|
+
NSNumber *bqDepthNum = attrs[BlockquoteDepthAttributeName];
|
|
237
|
+
NSInteger currentBqDepth = bqDepthNum ? [bqDepthNum integerValue] : -1;
|
|
238
|
+
NSString *blockquotePrefix = nil;
|
|
239
|
+
|
|
240
|
+
if (currentBqDepth >= 0) {
|
|
241
|
+
blockquotePrefix = buildBlockquotePrefix(currentBqDepth);
|
|
242
|
+
state.blockquoteDepth = currentBqDepth;
|
|
243
|
+
} else if (state.blockquoteDepth >= 0) {
|
|
244
|
+
ensureBlankLine(result);
|
|
245
|
+
state.blockquoteDepth = -1;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Lists
|
|
249
|
+
NSNumber *listDepthNum = attrs[@"ListDepth"];
|
|
250
|
+
NSNumber *listTypeNum = attrs[@"ListType"];
|
|
251
|
+
NSNumber *listItemNum = attrs[@"ListItemNumber"];
|
|
252
|
+
NSInteger currentListDepth = listDepthNum ? [listDepthNum integerValue] : -1;
|
|
253
|
+
|
|
254
|
+
if (currentListDepth >= 0) {
|
|
255
|
+
state.listDepth = currentListDepth;
|
|
256
|
+
} else if (state.listDepth >= 0) {
|
|
257
|
+
ensureBlankLine(result);
|
|
258
|
+
state.listDepth = -1;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Inline formatting
|
|
262
|
+
BOOL isBold, isItalic, isMonospace;
|
|
263
|
+
extractFontTraits(attrs, &isBold, &isItalic, &isMonospace);
|
|
264
|
+
|
|
265
|
+
NSNumber *strikethroughStyle = attrs[NSStrikethroughStyleAttributeName];
|
|
266
|
+
BOOL isStrikethrough = (strikethroughStyle != nil && [strikethroughStyle integerValue] != 0);
|
|
267
|
+
NSNumber *underlineStyle = attrs[NSUnderlineStyleAttributeName];
|
|
268
|
+
BOOL isUnderline = (underlineStyle != nil && [underlineStyle integerValue] != 0);
|
|
269
|
+
|
|
270
|
+
NSString *linkURL = attrs[NSLinkAttributeName];
|
|
271
|
+
NSString *segment = applyInlineFormatting(text, isBold, isItalic, isMonospace, isStrikethrough,
|
|
272
|
+
isUnderline, linkURL);
|
|
273
|
+
|
|
274
|
+
// Add block prefixes at line start
|
|
275
|
+
if (isAtLineStart(result)) {
|
|
276
|
+
NSMutableString *prefixedSegment = [NSMutableString string];
|
|
277
|
+
|
|
278
|
+
if (listDepthNum && ![text hasPrefix:@"\n"]) {
|
|
279
|
+
BOOL isOrdered = ([listTypeNum integerValue] == 1);
|
|
280
|
+
NSInteger itemNumber = listItemNum ? [listItemNum integerValue] : 1;
|
|
281
|
+
[prefixedSegment appendString:buildListPrefix(currentListDepth, isOrdered, itemNumber)];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (blockquotePrefix) {
|
|
285
|
+
[prefixedSegment insertString:blockquotePrefix atIndex:0];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
[prefixedSegment appendString:segment];
|
|
289
|
+
segment = prefixedSegment;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (state.needsBlankLine && result.length > 0) {
|
|
293
|
+
ensureBlankLine(result);
|
|
294
|
+
state.needsBlankLine = NO;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
[result appendString:segment];
|
|
298
|
+
}];
|
|
299
|
+
|
|
300
|
+
// Flush remaining heading
|
|
301
|
+
if (currentHeadingType && headingContent.length > 0) {
|
|
302
|
+
ensureBlankLine(result);
|
|
303
|
+
NSInteger level = [[currentHeadingType substringFromIndex:8] integerValue];
|
|
304
|
+
[result appendFormat:@"%@%@\n", buildHeadingPrefix(level), headingContent];
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return result.length > 0 ? result : nil;
|
|
308
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
3
|
+
|
|
4
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
5
|
+
|
|
6
|
+
__BEGIN_DECLS
|
|
7
|
+
|
|
8
|
+
extern NSAttributedString *kNewlineAttributedString;
|
|
9
|
+
|
|
10
|
+
NSMutableParagraphStyle *getOrCreateParagraphStyle(NSMutableAttributedString *output, NSUInteger index);
|
|
11
|
+
void applyParagraphSpacingAfter(NSMutableAttributedString *output, NSUInteger start, CGFloat marginBottom);
|
|
12
|
+
void applyParagraphSpacingBefore(NSMutableAttributedString *output, NSRange range, CGFloat marginTop);
|
|
13
|
+
NSUInteger applyBlockSpacingBefore(NSMutableAttributedString *output, NSUInteger insertionPoint, CGFloat marginTop);
|
|
14
|
+
void applyBlockSpacingAfter(NSMutableAttributedString *output, CGFloat marginBottom);
|
|
15
|
+
void applyLineHeight(NSMutableAttributedString *output, NSRange range, CGFloat lineHeight);
|
|
16
|
+
void applyTextAlignment(NSMutableAttributedString *output, NSRange range, NSTextAlignment textAlign);
|
|
17
|
+
NSTextAlignment textAlignmentFromString(NSString *textAlign);
|
|
18
|
+
|
|
19
|
+
__END_DECLS
|
|
20
|
+
|
|
21
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#import "ParagraphStyleUtils.h"
|
|
2
|
+
|
|
3
|
+
NSAttributedString *kNewlineAttributedString;
|
|
4
|
+
static NSParagraphStyle *kBlockSpacerTemplate;
|
|
5
|
+
|
|
6
|
+
__attribute__((constructor)) static void initParagraphStyleUtils(void)
|
|
7
|
+
{
|
|
8
|
+
kNewlineAttributedString = [[NSAttributedString alloc] initWithString:@"\n"];
|
|
9
|
+
|
|
10
|
+
NSMutableParagraphStyle *template = [[NSMutableParagraphStyle alloc] init];
|
|
11
|
+
template.minimumLineHeight = 1;
|
|
12
|
+
template.maximumLineHeight = 1;
|
|
13
|
+
kBlockSpacerTemplate = [template copy];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
NSMutableParagraphStyle *getOrCreateParagraphStyle(NSMutableAttributedString *output, NSUInteger index)
|
|
17
|
+
{
|
|
18
|
+
NSParagraphStyle *existing = [output attribute:NSParagraphStyleAttributeName atIndex:index effectiveRange:NULL];
|
|
19
|
+
return existing ? [existing mutableCopy] : [[NSMutableParagraphStyle alloc] init];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
void applyParagraphSpacingAfter(NSMutableAttributedString *output, NSUInteger start, CGFloat marginBottom)
|
|
23
|
+
{
|
|
24
|
+
[output appendAttributedString:kNewlineAttributedString];
|
|
25
|
+
|
|
26
|
+
NSMutableParagraphStyle *style = getOrCreateParagraphStyle(output, start);
|
|
27
|
+
style.paragraphSpacing = marginBottom;
|
|
28
|
+
|
|
29
|
+
NSRange range = NSMakeRange(start, output.length - start);
|
|
30
|
+
[output addAttribute:NSParagraphStyleAttributeName value:style range:range];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
void applyParagraphSpacingBefore(NSMutableAttributedString *output, NSRange range, CGFloat marginTop)
|
|
34
|
+
{
|
|
35
|
+
if (marginTop <= 0) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Note: paragraphSpacingBefore does not work at index 0 in TextKit.
|
|
40
|
+
// Leading spacing for the first element is handled by renderers via applyBlockSpacingBefore.
|
|
41
|
+
NSMutableParagraphStyle *style = getOrCreateParagraphStyle(output, range.location);
|
|
42
|
+
style.paragraphSpacingBefore = marginTop;
|
|
43
|
+
[output addAttribute:NSParagraphStyleAttributeName value:style range:range];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
NSUInteger applyBlockSpacingBefore(NSMutableAttributedString *output, NSUInteger insertionPoint, CGFloat marginTop)
|
|
47
|
+
{
|
|
48
|
+
if (marginTop <= 0) {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
NSMutableParagraphStyle *spacerStyle = [kBlockSpacerTemplate mutableCopy];
|
|
53
|
+
spacerStyle.paragraphSpacing = marginTop;
|
|
54
|
+
|
|
55
|
+
NSAttributedString *spacer =
|
|
56
|
+
[[NSAttributedString alloc] initWithString:@"\n" attributes:@{NSParagraphStyleAttributeName : spacerStyle}];
|
|
57
|
+
|
|
58
|
+
[output insertAttributedString:spacer atIndex:insertionPoint];
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
void applyBlockSpacingAfter(NSMutableAttributedString *output, CGFloat marginBottom)
|
|
63
|
+
{
|
|
64
|
+
if (marginBottom <= 0) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
NSUInteger spacerLocation = output.length;
|
|
69
|
+
[output appendAttributedString:kNewlineAttributedString];
|
|
70
|
+
|
|
71
|
+
NSMutableParagraphStyle *spacerStyle = [kBlockSpacerTemplate mutableCopy];
|
|
72
|
+
spacerStyle.paragraphSpacing = marginBottom;
|
|
73
|
+
|
|
74
|
+
[output addAttribute:NSParagraphStyleAttributeName value:spacerStyle range:NSMakeRange(spacerLocation, 1)];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
void applyLineHeight(NSMutableAttributedString *output, NSRange range, CGFloat lineHeight)
|
|
78
|
+
{
|
|
79
|
+
if (lineHeight <= 0) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
NSMutableParagraphStyle *style = getOrCreateParagraphStyle(output, range.location);
|
|
84
|
+
|
|
85
|
+
style.minimumLineHeight = lineHeight;
|
|
86
|
+
style.maximumLineHeight = lineHeight;
|
|
87
|
+
|
|
88
|
+
[output addAttribute:NSParagraphStyleAttributeName value:style range:range];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
void applyTextAlignment(NSMutableAttributedString *output, NSRange range, NSTextAlignment textAlign)
|
|
92
|
+
{
|
|
93
|
+
NSMutableParagraphStyle *style = getOrCreateParagraphStyle(output, range.location);
|
|
94
|
+
style.alignment = textAlign;
|
|
95
|
+
[output addAttribute:NSParagraphStyleAttributeName value:style range:range];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
NSTextAlignment textAlignmentFromString(NSString *textAlign)
|
|
99
|
+
{
|
|
100
|
+
if ([textAlign isEqualToString:@"center"]) {
|
|
101
|
+
return NSTextAlignmentCenter;
|
|
102
|
+
} else if ([textAlign isEqualToString:@"right"]) {
|
|
103
|
+
return NSTextAlignmentRight;
|
|
104
|
+
} else if ([textAlign isEqualToString:@"justify"]) {
|
|
105
|
+
return NSTextAlignmentJustified;
|
|
106
|
+
} else if ([textAlign isEqualToString:@"auto"]) {
|
|
107
|
+
return NSTextAlignmentNatural;
|
|
108
|
+
}
|
|
109
|
+
// Default to left for "left" or any unknown value
|
|
110
|
+
return NSTextAlignmentLeft;
|
|
111
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#import <Foundation/Foundation.h>
|
|
3
|
+
#import <UIKit/UIKit.h>
|
|
4
|
+
|
|
5
|
+
@class StyleConfig;
|
|
6
|
+
|
|
7
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
8
|
+
|
|
9
|
+
#ifdef __cplusplus
|
|
10
|
+
extern "C" {
|
|
11
|
+
#endif
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Copies attributed string to pasteboard with multiple representations
|
|
15
|
+
* (plain text, Markdown, HTML, RTFD, RTF). Receiving apps pick the richest format they support.
|
|
16
|
+
*/
|
|
17
|
+
void copyAttributedStringToPasteboard(NSAttributedString *attributedString, NSString *_Nullable markdown,
|
|
18
|
+
StyleConfig *_Nullable styleConfig);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extracts markdown for the given range.
|
|
22
|
+
* Full selection returns cached markdown; partial selection reverse-engineers from attributes.
|
|
23
|
+
*/
|
|
24
|
+
NSString *_Nullable markdownForRange(NSAttributedString *attributedText, NSRange range,
|
|
25
|
+
NSString *_Nullable cachedMarkdown);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns remote image URLs (http/https only) from EnrichedMarkdownImageAttachments in the given range.
|
|
29
|
+
*/
|
|
30
|
+
NSArray<NSString *> *imageURLsInRange(NSAttributedString *attributedText, NSRange range);
|
|
31
|
+
|
|
32
|
+
#ifdef __cplusplus
|
|
33
|
+
}
|
|
34
|
+
#endif
|
|
35
|
+
|
|
36
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#import "PasteboardUtils.h"
|
|
2
|
+
#import "EnrichedMarkdownImageAttachment.h"
|
|
3
|
+
#import "HTMLGenerator.h"
|
|
4
|
+
#import "MarkdownExtractor.h"
|
|
5
|
+
#import "RTFExportUtils.h"
|
|
6
|
+
#import "StyleConfig.h"
|
|
7
|
+
#import <UIKit/UIPasteboard.h>
|
|
8
|
+
|
|
9
|
+
static NSString *const kUTIPlainText = @"public.utf8-plain-text";
|
|
10
|
+
static NSString *const kUTIMarkdown = @"net.daringfireball.markdown";
|
|
11
|
+
static NSString *const kUTIHTML = @"public.html";
|
|
12
|
+
static NSString *const kUTIRTFD = @"com.apple.rtfd";
|
|
13
|
+
static NSString *const kUTIFlatRTFD = @"com.apple.flat-rtfd";
|
|
14
|
+
static NSString *const kUTIRTF = @"public.rtf";
|
|
15
|
+
|
|
16
|
+
#pragma mark - Private Helpers
|
|
17
|
+
|
|
18
|
+
static void addRTFData(NSMutableDictionary *items, NSAttributedString *attributedString, NSRange range,
|
|
19
|
+
NSString *documentType, NSString *uti)
|
|
20
|
+
{
|
|
21
|
+
NSError *error = nil;
|
|
22
|
+
NSData *data = [attributedString dataFromRange:range
|
|
23
|
+
documentAttributes:@{NSDocumentTypeDocumentAttribute : documentType}
|
|
24
|
+
error:&error];
|
|
25
|
+
if (data && !error) {
|
|
26
|
+
items[uti] = data;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static void addRTFDData(NSMutableDictionary *items, NSAttributedString *attributedString, NSRange range)
|
|
31
|
+
{
|
|
32
|
+
NSError *error = nil;
|
|
33
|
+
NSFileWrapper *wrapper =
|
|
34
|
+
[attributedString fileWrapperFromRange:range
|
|
35
|
+
documentAttributes:@{NSDocumentTypeDocumentAttribute : NSRTFDTextDocumentType}
|
|
36
|
+
error:&error];
|
|
37
|
+
if (wrapper && !error) {
|
|
38
|
+
NSData *data = [wrapper serializedRepresentation];
|
|
39
|
+
if (data) {
|
|
40
|
+
items[kUTIRTFD] = data;
|
|
41
|
+
items[kUTIFlatRTFD] = data;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static void addHTMLData(NSMutableDictionary *items, NSAttributedString *attributedString, StyleConfig *styleConfig)
|
|
47
|
+
{
|
|
48
|
+
NSString *html = generateHTML(attributedString, styleConfig);
|
|
49
|
+
if (html) {
|
|
50
|
+
NSData *data = [html dataUsingEncoding:NSUTF8StringEncoding];
|
|
51
|
+
if (data) {
|
|
52
|
+
items[kUTIHTML] = data;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#pragma mark - Public API
|
|
58
|
+
|
|
59
|
+
void copyAttributedStringToPasteboard(NSAttributedString *attributedString, NSString *_Nullable markdown,
|
|
60
|
+
StyleConfig *_Nullable styleConfig)
|
|
61
|
+
{
|
|
62
|
+
if (!attributedString || attributedString.length == 0)
|
|
63
|
+
return;
|
|
64
|
+
|
|
65
|
+
NSMutableDictionary *items = [NSMutableDictionary dictionary];
|
|
66
|
+
|
|
67
|
+
items[kUTIPlainText] = attributedString.string;
|
|
68
|
+
|
|
69
|
+
if (markdown.length > 0) {
|
|
70
|
+
items[kUTIMarkdown] = markdown;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (styleConfig) {
|
|
74
|
+
addHTMLData(items, attributedString, styleConfig);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// RTF export requires preprocessing (backgrounds, markers, normalized spacing)
|
|
78
|
+
NSAttributedString *rtfPrepared = prepareAttributedStringForRTFExport(attributedString, styleConfig);
|
|
79
|
+
NSRange rtfRange = NSMakeRange(0, rtfPrepared.length);
|
|
80
|
+
|
|
81
|
+
addRTFDData(items, rtfPrepared, rtfRange);
|
|
82
|
+
addRTFData(items, rtfPrepared, rtfRange, NSRTFTextDocumentType, kUTIRTF);
|
|
83
|
+
|
|
84
|
+
[[UIPasteboard generalPasteboard] setItems:@[ items ]];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
#pragma mark - Content Extraction
|
|
88
|
+
|
|
89
|
+
NSString *_Nullable markdownForRange(NSAttributedString *attributedText, NSRange range,
|
|
90
|
+
NSString *_Nullable cachedMarkdown)
|
|
91
|
+
{
|
|
92
|
+
if (!cachedMarkdown || range.length == 0)
|
|
93
|
+
return nil;
|
|
94
|
+
|
|
95
|
+
if (!attributedText || range.location >= attributedText.length)
|
|
96
|
+
return nil;
|
|
97
|
+
|
|
98
|
+
range.length = MIN(range.length, attributedText.length - range.location);
|
|
99
|
+
|
|
100
|
+
// Full selection: use cached markdown directly
|
|
101
|
+
BOOL isFullSelection = (range.location == 0 && range.length >= attributedText.length - 1);
|
|
102
|
+
if (isFullSelection) {
|
|
103
|
+
return cachedMarkdown;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Partial selection: reverse-engineer from attributes
|
|
107
|
+
return extractMarkdownFromAttributedString(attributedText, range);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
NSArray<NSString *> *imageURLsInRange(NSAttributedString *attributedText, NSRange range)
|
|
111
|
+
{
|
|
112
|
+
if (!attributedText || range.location == NSNotFound || range.length == 0 || range.location >= attributedText.length) {
|
|
113
|
+
return @[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
range.length = MIN(range.length, attributedText.length - range.location);
|
|
117
|
+
|
|
118
|
+
NSMutableArray<NSString *> *urls = [NSMutableArray array];
|
|
119
|
+
|
|
120
|
+
[attributedText enumerateAttribute:NSAttachmentAttributeName
|
|
121
|
+
inRange:range
|
|
122
|
+
options:0
|
|
123
|
+
usingBlock:^(id value, NSRange r, BOOL *stop) {
|
|
124
|
+
if (![value isKindOfClass:[EnrichedMarkdownImageAttachment class]])
|
|
125
|
+
return;
|
|
126
|
+
|
|
127
|
+
NSString *url = ((EnrichedMarkdownImageAttachment *)value).imageURL;
|
|
128
|
+
if ([url hasPrefix:@"http://"] || [url hasPrefix:@"https://"]) {
|
|
129
|
+
[urls addObject:url];
|
|
130
|
+
}
|
|
131
|
+
}];
|
|
132
|
+
|
|
133
|
+
return urls;
|
|
134
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#import <Foundation/Foundation.h>
|
|
3
|
+
#import <UIKit/UIKit.h>
|
|
4
|
+
|
|
5
|
+
@class StyleConfig;
|
|
6
|
+
|
|
7
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
8
|
+
|
|
9
|
+
#ifdef __cplusplus
|
|
10
|
+
extern "C" {
|
|
11
|
+
#endif
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Prepares NSAttributedString for RTF/RTFD export by adding backgrounds,
|
|
15
|
+
* inserting text markers (>, •, 1.), and normalizing paragraph styles.
|
|
16
|
+
*/
|
|
17
|
+
NSAttributedString *prepareAttributedStringForRTFExport(NSAttributedString *attributedString,
|
|
18
|
+
StyleConfig *_Nullable styleConfig);
|
|
19
|
+
|
|
20
|
+
#ifdef __cplusplus
|
|
21
|
+
}
|
|
22
|
+
#endif
|
|
23
|
+
|
|
24
|
+
NS_ASSUME_NONNULL_END
|