react-native-enriched-markdown 0.1.0 → 0.1.1
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 +479 -0
- package/ReactNativeEnrichedMarkdown.podspec +27 -0
- package/android/build.gradle +101 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerDelegate.java +39 -0
- package/android/generated/java/com/facebook/react/viewmanagers/EnrichedMarkdownTextManagerInterface.java +21 -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 +24 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/EventEmitters.h +25 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.cpp +57 -0
- package/android/generated/jni/react/renderer/components/EnrichedMarkdownTextSpec/Props.h +1164 -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 +203 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownText.kt +153 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextLayoutManager.kt +30 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/EnrichedMarkdownTextManager.kt +119 -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 +165 -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 +29 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/parser/Parser.kt +48 -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 +89 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeBlockRenderer.kt +105 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/CodeRenderer.kt +35 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/DocumentRenderer.kt +15 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/EmphasisRenderer.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/HeadingRenderer.kt +54 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ImageRenderer.kt +52 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LineBreakRenderer.kt +15 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/LinkRenderer.kt +28 -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 +58 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ListRenderer.kt +69 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/NodeRenderer.kt +99 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ParagraphRenderer.kt +66 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/Renderer.kt +95 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/SpanStyleCache.kt +85 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/StrongRenderer.kt +26 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/TextRenderer.kt +29 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/renderer/ThematicBreakRenderer.kt +44 -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 +320 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LineHeightSpan.kt +36 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/spans/LinkSpan.kt +37 -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/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 +10 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/BlockquoteStyle.kt +48 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/CodeBlockStyle.kt +51 -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 +29 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ImageStyle.kt +21 -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 +54 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ParagraphStyle.kt +29 -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 +180 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/StyleParser.kt +75 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/styles/ThematicBreakStyle.kt +23 -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 +809 -0
- package/android/src/main/java/com/swmansion/enriched/markdown/utils/MarkdownExtractor.kt +365 -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 +181 -0
- package/android/src/main/jni/CMakeLists.txt +82 -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 +33 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextShadowNode.h +49 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.cpp +9 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/MarkdownTextState.h +25 -0
- package/android/src/main/jni/react/renderer/components/EnrichedMarkdownTextSpec/conversions.h +19 -0
- package/cpp/md4c/md4c.c +6492 -0
- package/cpp/md4c/md4c.h +402 -0
- package/cpp/parser/MD4CParser.cpp +314 -0
- package/cpp/parser/MD4CParser.hpp +23 -0
- package/cpp/parser/MarkdownASTNode.hpp +49 -0
- package/ios/EnrichedMarkdownText.h +18 -0
- package/ios/EnrichedMarkdownText.mm +1074 -0
- package/ios/attachments/ImageAttachment.h +23 -0
- package/ios/attachments/ImageAttachment.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 +24 -0
- package/ios/generated/EnrichedMarkdownTextSpec/EventEmitters.h +25 -0
- package/ios/generated/EnrichedMarkdownTextSpec/Props.cpp +57 -0
- package/ios/generated/EnrichedMarkdownTextSpec/Props.h +1164 -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 +33 -0
- package/ios/parser/MarkdownASTNode.m +32 -0
- package/ios/parser/MarkdownParser.h +8 -0
- package/ios/parser/MarkdownParser.mm +13 -0
- package/ios/parser/MarkdownParserBridge.mm +110 -0
- package/ios/renderer/AttributedRenderer.h +9 -0
- package/ios/renderer/AttributedRenderer.m +119 -0
- package/ios/renderer/BlockquoteRenderer.h +7 -0
- package/ios/renderer/BlockquoteRenderer.m +159 -0
- package/ios/renderer/CodeBlockRenderer.h +10 -0
- package/ios/renderer/CodeBlockRenderer.m +89 -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 +98 -0
- package/ios/renderer/ImageRenderer.h +12 -0
- package/ios/renderer/ImageRenderer.m +62 -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 +91 -0
- package/ios/renderer/ListRenderer.h +13 -0
- package/ios/renderer/ListRenderer.m +67 -0
- package/ios/renderer/NodeRenderer.h +8 -0
- package/ios/renderer/ParagraphRenderer.h +7 -0
- package/ios/renderer/ParagraphRenderer.m +69 -0
- package/ios/renderer/RenderContext.h +88 -0
- package/ios/renderer/RenderContext.m +248 -0
- package/ios/renderer/RendererFactory.h +12 -0
- package/ios/renderer/RendererFactory.m +110 -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/styles/StyleConfig.h +228 -0
- package/ios/styles/StyleConfig.mm +1467 -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 +87 -0
- package/ios/utils/EditMenuUtils.h +22 -0
- package/ios/utils/EditMenuUtils.m +118 -0
- package/ios/utils/FontUtils.h +20 -0
- package/ios/utils/FontUtils.m +13 -0
- package/ios/utils/HTMLGenerator.h +20 -0
- package/ios/utils/HTMLGenerator.m +779 -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/MarkdownExtractor.h +17 -0
- package/ios/utils/MarkdownExtractor.m +295 -0
- package/ios/utils/ParagraphStyleUtils.h +13 -0
- package/ios/utils/ParagraphStyleUtils.m +56 -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 +34 -0
- package/lib/module/EnrichedMarkdownText.js.map +1 -0
- package/lib/module/EnrichedMarkdownTextNativeComponent.ts +130 -0
- package/lib/module/index.js +5 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/normalizeMarkdownStyle.js +340 -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 +101 -0
- package/lib/typescript/src/EnrichedMarkdownText.d.ts.map +1 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts +111 -0
- package/lib/typescript/src/EnrichedMarkdownTextNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +5 -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 +152 -0
- package/src/EnrichedMarkdownTextNativeComponent.ts +130 -0
- package/src/index.tsx +7 -0
- package/src/normalizeMarkdownStyle.ts +377 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
3
|
+
|
|
4
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
5
|
+
|
|
6
|
+
static NSString *const CodeBlockAttributeName = @"CodeBlock";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Checks if the last element in the attributed string is a code block.
|
|
10
|
+
* Used to compensate for iOS text APIs not measuring/drawing trailing newlines with custom line heights.
|
|
11
|
+
*/
|
|
12
|
+
static inline BOOL isLastElementCodeBlock(NSAttributedString *text)
|
|
13
|
+
{
|
|
14
|
+
if (text.length == 0)
|
|
15
|
+
return NO;
|
|
16
|
+
|
|
17
|
+
NSRange lastContent = [text.string rangeOfCharacterFromSet:[[NSCharacterSet newlineCharacterSet] invertedSet]
|
|
18
|
+
options:NSBackwardsSearch];
|
|
19
|
+
if (lastContent.location == NSNotFound)
|
|
20
|
+
return NO;
|
|
21
|
+
|
|
22
|
+
NSNumber *isCodeBlock = [text attribute:CodeBlockAttributeName atIndex:lastContent.location effectiveRange:nil];
|
|
23
|
+
if (!isCodeBlock.boolValue)
|
|
24
|
+
return NO;
|
|
25
|
+
|
|
26
|
+
NSRange codeBlockRange;
|
|
27
|
+
[text attribute:CodeBlockAttributeName atIndex:lastContent.location effectiveRange:&codeBlockRange];
|
|
28
|
+
return NSMaxRange(codeBlockRange) == text.length;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Checks if the last element in the attributed string is an image attachment.
|
|
33
|
+
* Used to compensate for iOS text attachment baseline spacing issues.
|
|
34
|
+
*/
|
|
35
|
+
static inline BOOL isLastElementImage(NSAttributedString *text)
|
|
36
|
+
{
|
|
37
|
+
if (text.length == 0)
|
|
38
|
+
return NO;
|
|
39
|
+
|
|
40
|
+
NSRange lastContent = [text.string rangeOfCharacterFromSet:[[NSCharacterSet newlineCharacterSet] invertedSet]
|
|
41
|
+
options:NSBackwardsSearch];
|
|
42
|
+
if (lastContent.location == NSNotFound)
|
|
43
|
+
return NO;
|
|
44
|
+
|
|
45
|
+
unichar lastChar = [text.string characterAtIndex:lastContent.location];
|
|
46
|
+
if (lastChar != 0xFFFC)
|
|
47
|
+
return NO;
|
|
48
|
+
|
|
49
|
+
id attachment = [text attribute:NSAttachmentAttributeName atIndex:lastContent.location effectiveRange:nil];
|
|
50
|
+
return attachment != nil;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
3
|
+
|
|
4
|
+
@class StyleConfig;
|
|
5
|
+
|
|
6
|
+
@interface ListMarkerDrawer : NSObject
|
|
7
|
+
|
|
8
|
+
- (instancetype)initWithConfig:(StyleConfig *)config;
|
|
9
|
+
|
|
10
|
+
- (void)drawMarkersForGlyphRange:(NSRange)glyphsToShow
|
|
11
|
+
layoutManager:(NSLayoutManager *)layoutManager
|
|
12
|
+
textContainer:(NSTextContainer *)textContainer
|
|
13
|
+
atPoint:(CGPoint)origin;
|
|
14
|
+
|
|
15
|
+
@end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#import "ListMarkerDrawer.h"
|
|
2
|
+
#import "ListItemRenderer.h"
|
|
3
|
+
#import "RenderContext.h"
|
|
4
|
+
#import "StyleConfig.h"
|
|
5
|
+
|
|
6
|
+
// Reference external symbols defined in ListItemRenderer.m
|
|
7
|
+
extern NSString *const ListDepthAttribute;
|
|
8
|
+
extern NSString *const ListTypeAttribute;
|
|
9
|
+
extern NSString *const ListItemNumberAttribute;
|
|
10
|
+
|
|
11
|
+
@implementation ListMarkerDrawer {
|
|
12
|
+
StyleConfig *_config;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
- (instancetype)initWithConfig:(StyleConfig *)config
|
|
16
|
+
{
|
|
17
|
+
if (self = [super init]) {
|
|
18
|
+
_config = config;
|
|
19
|
+
}
|
|
20
|
+
return self;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
- (void)drawMarkersForGlyphRange:(NSRange)glyphsToShow
|
|
24
|
+
layoutManager:(NSLayoutManager *)layoutManager
|
|
25
|
+
textContainer:(NSTextContainer *)textContainer
|
|
26
|
+
atPoint:(CGPoint)origin
|
|
27
|
+
{
|
|
28
|
+
NSTextStorage *storage = layoutManager.textStorage;
|
|
29
|
+
if (!storage || storage.length == 0)
|
|
30
|
+
return;
|
|
31
|
+
|
|
32
|
+
// Cache gap and track paragraphs to prevent double-drawing on wrapped lines
|
|
33
|
+
CGFloat gap = [_config effectiveListGapWidth];
|
|
34
|
+
NSMutableSet *drawnParagraphs = [NSMutableSet set];
|
|
35
|
+
|
|
36
|
+
[layoutManager
|
|
37
|
+
enumerateLineFragmentsForGlyphRange:glyphsToShow
|
|
38
|
+
usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer *container,
|
|
39
|
+
NSRange glyphRange, BOOL *stop) {
|
|
40
|
+
NSRange charRange = [layoutManager characterRangeForGlyphRange:glyphRange
|
|
41
|
+
actualGlyphRange:NULL];
|
|
42
|
+
if (charRange.location == NSNotFound)
|
|
43
|
+
return;
|
|
44
|
+
|
|
45
|
+
// 1. Fetch all attributes at once for efficiency
|
|
46
|
+
NSDictionary *attrs = [storage attributesAtIndex:charRange.location
|
|
47
|
+
effectiveRange:NULL];
|
|
48
|
+
if (!attrs[ListDepthAttribute])
|
|
49
|
+
return;
|
|
50
|
+
|
|
51
|
+
// 2. Identify the start of the paragraph
|
|
52
|
+
NSRange paraRange = [storage.string paragraphRangeForRange:charRange];
|
|
53
|
+
if (charRange.location != paraRange.location ||
|
|
54
|
+
[drawnParagraphs containsObject:@(paraRange.location)])
|
|
55
|
+
return;
|
|
56
|
+
[drawnParagraphs addObject:@(paraRange.location)];
|
|
57
|
+
|
|
58
|
+
// 3. Calculate Layout Coordinates
|
|
59
|
+
CGPoint glyphLoc = [layoutManager locationForGlyphAtIndex:glyphRange.location];
|
|
60
|
+
CGFloat baselineY = origin.y + rect.origin.y + glyphLoc.y;
|
|
61
|
+
CGFloat textStartX = origin.x + usedRect.origin.x;
|
|
62
|
+
|
|
63
|
+
// 4. Draw marker based on type
|
|
64
|
+
if ([attrs[ListTypeAttribute] integerValue] == ListTypeUnordered) {
|
|
65
|
+
UIFont *font = attrs[NSFontAttributeName] ?: [self defaultFont];
|
|
66
|
+
[self drawBulletAtX:textStartX - gap centerY:baselineY - (font.xHeight / 2.0)];
|
|
67
|
+
} else {
|
|
68
|
+
[self drawOrderedMarkerAtX:textStartX - gap attrs:attrs baselineY:baselineY];
|
|
69
|
+
}
|
|
70
|
+
}];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#pragma mark - Drawing Helpers
|
|
74
|
+
|
|
75
|
+
- (void)drawBulletAtX:(CGFloat)x centerY:(CGFloat)y
|
|
76
|
+
{
|
|
77
|
+
[self
|
|
78
|
+
executeDrawing:^(CGContextRef ctx) {
|
|
79
|
+
[[_config listStyleBulletColor] ?: [UIColor blackColor] setFill];
|
|
80
|
+
CGFloat size = [_config listStyleBulletSize];
|
|
81
|
+
CGContextFillEllipseInRect(ctx, CGRectMake(x - size / 2.0, y - size / 2.0, size, size));
|
|
82
|
+
}
|
|
83
|
+
atX:x
|
|
84
|
+
y:y];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
- (void)drawOrderedMarkerAtX:(CGFloat)rightBoundaryX attrs:(NSDictionary *)attrs baselineY:(CGFloat)baselineY
|
|
88
|
+
{
|
|
89
|
+
NSNumber *num = attrs[ListItemNumberAttribute];
|
|
90
|
+
if (!num)
|
|
91
|
+
return;
|
|
92
|
+
|
|
93
|
+
NSString *text = [NSString stringWithFormat:@"%ld.", (long)num.integerValue];
|
|
94
|
+
UIFont *font = [_config listMarkerFont] ?: [self defaultFont];
|
|
95
|
+
|
|
96
|
+
NSDictionary *mAttrs = @{
|
|
97
|
+
NSFontAttributeName : font,
|
|
98
|
+
NSForegroundColorAttributeName : [_config listStyleMarkerColor] ?: [UIColor blackColor]
|
|
99
|
+
};
|
|
100
|
+
CGSize size = [text sizeWithAttributes:mAttrs];
|
|
101
|
+
|
|
102
|
+
if ([self isValidX:rightBoundaryX - size.width y:baselineY]) {
|
|
103
|
+
[text drawAtPoint:CGPointMake(rightBoundaryX - size.width, baselineY - font.ascender) withAttributes:mAttrs];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
- (void)executeDrawing:(void (^)(CGContextRef))block atX:(CGFloat)x y:(CGFloat)y
|
|
108
|
+
{
|
|
109
|
+
CGContextRef ctx = UIGraphicsGetCurrentContext();
|
|
110
|
+
if (ctx && [self isValidX:x y:y]) {
|
|
111
|
+
CGContextSaveGState(ctx);
|
|
112
|
+
block(ctx);
|
|
113
|
+
CGContextRestoreGState(ctx);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
- (UIFont *)defaultFont
|
|
118
|
+
{
|
|
119
|
+
return [UIFont systemFontOfSize:[_config listStyleFontSize]];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
- (BOOL)isValidX:(CGFloat)x y:(CGFloat)y
|
|
123
|
+
{
|
|
124
|
+
return !isnan(x) && !isinf(x) && !isnan(y) && !isinf(y);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
@end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
3
|
+
|
|
4
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
5
|
+
|
|
6
|
+
#ifdef __cplusplus
|
|
7
|
+
extern "C" {
|
|
8
|
+
#endif
|
|
9
|
+
|
|
10
|
+
/// Extracts markdown from an attributed string (best-effort reconstruction).
|
|
11
|
+
NSString *_Nullable extractMarkdownFromAttributedString(NSAttributedString *attributedText, NSRange range);
|
|
12
|
+
|
|
13
|
+
#ifdef __cplusplus
|
|
14
|
+
}
|
|
15
|
+
#endif
|
|
16
|
+
|
|
17
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#import "MarkdownExtractor.h"
|
|
2
|
+
#import "BlockquoteBorder.h"
|
|
3
|
+
#import "ImageAttachment.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, NSString *linkURL)
|
|
71
|
+
{
|
|
72
|
+
NSMutableString *result = [NSMutableString stringWithString:text];
|
|
73
|
+
|
|
74
|
+
// Innermost first
|
|
75
|
+
if (isMonospace && !linkURL) {
|
|
76
|
+
result = [NSMutableString stringWithFormat:@"`%@`", result];
|
|
77
|
+
}
|
|
78
|
+
if (isItalic) {
|
|
79
|
+
result = [NSMutableString stringWithFormat:@"*%@*", result];
|
|
80
|
+
}
|
|
81
|
+
if (isBold) {
|
|
82
|
+
result = [NSMutableString stringWithFormat:@"**%@**", result];
|
|
83
|
+
}
|
|
84
|
+
if (linkURL) {
|
|
85
|
+
result = [NSMutableString stringWithFormat:@"[%@](%@)", text, linkURL];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#pragma mark - Main Extraction Function
|
|
92
|
+
|
|
93
|
+
NSString *_Nullable extractMarkdownFromAttributedString(NSAttributedString *attributedText, NSRange range)
|
|
94
|
+
{
|
|
95
|
+
if (!attributedText || range.length == 0 || range.location >= attributedText.length) {
|
|
96
|
+
return nil;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
range.length = MIN(range.length, attributedText.length - range.location);
|
|
100
|
+
|
|
101
|
+
NSMutableString *result = [NSMutableString string];
|
|
102
|
+
|
|
103
|
+
// Headings may span multiple attribute runs
|
|
104
|
+
__block NSString *currentHeadingType = nil;
|
|
105
|
+
__block NSMutableString *headingContent = nil;
|
|
106
|
+
__block ExtractionState state = {.blockquoteDepth = -1, .listDepth = -1, .needsBlankLine = NO};
|
|
107
|
+
|
|
108
|
+
void (^flushHeading)(void) = ^{
|
|
109
|
+
if (!currentHeadingType || headingContent.length == 0)
|
|
110
|
+
return;
|
|
111
|
+
|
|
112
|
+
ensureBlankLine(result);
|
|
113
|
+
NSInteger level = [[currentHeadingType substringFromIndex:8] integerValue];
|
|
114
|
+
[result appendFormat:@"%@%@\n", buildHeadingPrefix(level), headingContent];
|
|
115
|
+
|
|
116
|
+
currentHeadingType = nil;
|
|
117
|
+
headingContent = nil;
|
|
118
|
+
state.needsBlankLine = YES;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
[attributedText
|
|
122
|
+
enumerateAttributesInRange:range
|
|
123
|
+
options:0
|
|
124
|
+
usingBlock:^(NSDictionary<NSAttributedStringKey, id> *attrs, NSRange attrRange, BOOL *stop) {
|
|
125
|
+
NSString *text = [[attributedText attributedSubstringFromRange:attrRange] string];
|
|
126
|
+
if (text.length == 0)
|
|
127
|
+
return;
|
|
128
|
+
|
|
129
|
+
// Images and Thematic Breaks
|
|
130
|
+
NSTextAttachment *attachment = attrs[NSAttachmentAttributeName];
|
|
131
|
+
if ([attachment isKindOfClass:[ImageAttachment class]]) {
|
|
132
|
+
ImageAttachment *img = (ImageAttachment *)attachment;
|
|
133
|
+
if (!img.imageURL)
|
|
134
|
+
return;
|
|
135
|
+
|
|
136
|
+
if (img.isInline) {
|
|
137
|
+
[result appendFormat:@"", img.imageURL];
|
|
138
|
+
} else {
|
|
139
|
+
ensureBlankLine(result);
|
|
140
|
+
[result appendFormat:@"\n", img.imageURL];
|
|
141
|
+
state.needsBlankLine = YES;
|
|
142
|
+
state.blockquoteDepth = -1;
|
|
143
|
+
state.listDepth = -1;
|
|
144
|
+
}
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if ([attachment isKindOfClass:[ThematicBreakAttachment class]]) {
|
|
149
|
+
ensureBlankLine(result);
|
|
150
|
+
[result appendString:@"---\n"];
|
|
151
|
+
state.needsBlankLine = YES;
|
|
152
|
+
state.blockquoteDepth = -1;
|
|
153
|
+
state.listDepth = -1;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if ([text isEqualToString:@"\uFFFC"])
|
|
158
|
+
return;
|
|
159
|
+
|
|
160
|
+
// Newlines
|
|
161
|
+
if ([text isEqualToString:@"\n"] || [text isEqualToString:@"\n\n"]) {
|
|
162
|
+
NSNumber *bqDepth = attrs[BlockquoteDepthAttributeName];
|
|
163
|
+
NSNumber *listDepth = attrs[@"ListDepth"];
|
|
164
|
+
BOOL inBlockquote = (bqDepth != nil);
|
|
165
|
+
BOOL inList = (listDepth != nil);
|
|
166
|
+
|
|
167
|
+
if (!inBlockquote && state.blockquoteDepth >= 0) {
|
|
168
|
+
ensureBlankLine(result);
|
|
169
|
+
state.blockquoteDepth = -1;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!inList && state.listDepth >= 0) {
|
|
174
|
+
ensureBlankLine(result);
|
|
175
|
+
state.listDepth = -1;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (inBlockquote || inList) {
|
|
180
|
+
if (![result hasSuffix:@"\n"]) {
|
|
181
|
+
[result appendString:@"\n"];
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
ensureBlankLine(result);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Headings
|
|
191
|
+
NSString *markdownType = attrs[MarkdownTypeAttributeName];
|
|
192
|
+
|
|
193
|
+
if (markdownType && [markdownType hasPrefix:@"heading-"]) {
|
|
194
|
+
if (![markdownType isEqualToString:currentHeadingType]) {
|
|
195
|
+
flushHeading();
|
|
196
|
+
currentHeadingType = markdownType;
|
|
197
|
+
headingContent = [NSMutableString string];
|
|
198
|
+
}
|
|
199
|
+
[headingContent
|
|
200
|
+
appendString:[text stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]]];
|
|
201
|
+
return;
|
|
202
|
+
} else if (currentHeadingType) {
|
|
203
|
+
flushHeading();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Code blocks
|
|
207
|
+
NSNumber *isCodeBlock = attrs[CodeBlockAttributeName];
|
|
208
|
+
if ([isCodeBlock boolValue]) {
|
|
209
|
+
if (state.needsBlankLine) {
|
|
210
|
+
ensureBlankLine(result);
|
|
211
|
+
state.needsBlankLine = NO;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
BOOL needsFence = (result.length == 0) || [result hasSuffix:@"\n\n"];
|
|
215
|
+
if (needsFence) {
|
|
216
|
+
[result appendString:@"```\n"];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
[result appendString:text];
|
|
220
|
+
|
|
221
|
+
if ([text hasSuffix:@"\n"]) {
|
|
222
|
+
[result appendString:@"```\n"];
|
|
223
|
+
state.needsBlankLine = YES;
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Blockquotes
|
|
229
|
+
NSNumber *bqDepthNum = attrs[BlockquoteDepthAttributeName];
|
|
230
|
+
NSInteger currentBqDepth = bqDepthNum ? [bqDepthNum integerValue] : -1;
|
|
231
|
+
NSString *blockquotePrefix = nil;
|
|
232
|
+
|
|
233
|
+
if (currentBqDepth >= 0) {
|
|
234
|
+
blockquotePrefix = buildBlockquotePrefix(currentBqDepth);
|
|
235
|
+
state.blockquoteDepth = currentBqDepth;
|
|
236
|
+
} else if (state.blockquoteDepth >= 0) {
|
|
237
|
+
ensureBlankLine(result);
|
|
238
|
+
state.blockquoteDepth = -1;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Lists
|
|
242
|
+
NSNumber *listDepthNum = attrs[@"ListDepth"];
|
|
243
|
+
NSNumber *listTypeNum = attrs[@"ListType"];
|
|
244
|
+
NSNumber *listItemNum = attrs[@"ListItemNumber"];
|
|
245
|
+
NSInteger currentListDepth = listDepthNum ? [listDepthNum integerValue] : -1;
|
|
246
|
+
|
|
247
|
+
if (currentListDepth >= 0) {
|
|
248
|
+
state.listDepth = currentListDepth;
|
|
249
|
+
} else if (state.listDepth >= 0) {
|
|
250
|
+
ensureBlankLine(result);
|
|
251
|
+
state.listDepth = -1;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Inline formatting
|
|
255
|
+
BOOL isBold, isItalic, isMonospace;
|
|
256
|
+
extractFontTraits(attrs, &isBold, &isItalic, &isMonospace);
|
|
257
|
+
|
|
258
|
+
NSString *linkURL = attrs[NSLinkAttributeName];
|
|
259
|
+
NSString *segment = applyInlineFormatting(text, isBold, isItalic, isMonospace, linkURL);
|
|
260
|
+
|
|
261
|
+
// Add block prefixes at line start
|
|
262
|
+
if (isAtLineStart(result)) {
|
|
263
|
+
NSMutableString *prefixedSegment = [NSMutableString string];
|
|
264
|
+
|
|
265
|
+
if (listDepthNum && ![text hasPrefix:@"\n"]) {
|
|
266
|
+
BOOL isOrdered = ([listTypeNum integerValue] == 1);
|
|
267
|
+
NSInteger itemNumber = listItemNum ? [listItemNum integerValue] : 1;
|
|
268
|
+
[prefixedSegment appendString:buildListPrefix(currentListDepth, isOrdered, itemNumber)];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (blockquotePrefix) {
|
|
272
|
+
[prefixedSegment insertString:blockquotePrefix atIndex:0];
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
[prefixedSegment appendString:segment];
|
|
276
|
+
segment = prefixedSegment;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (state.needsBlankLine && result.length > 0) {
|
|
280
|
+
ensureBlankLine(result);
|
|
281
|
+
state.needsBlankLine = NO;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
[result appendString:segment];
|
|
285
|
+
}];
|
|
286
|
+
|
|
287
|
+
// Flush remaining heading
|
|
288
|
+
if (currentHeadingType && headingContent.length > 0) {
|
|
289
|
+
ensureBlankLine(result);
|
|
290
|
+
NSInteger level = [[currentHeadingType substringFromIndex:8] integerValue];
|
|
291
|
+
[result appendFormat:@"%@%@\n", buildHeadingPrefix(level), headingContent];
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return result.length > 0 ? result : nil;
|
|
295
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
3
|
+
|
|
4
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
5
|
+
|
|
6
|
+
extern NSAttributedString *kNewlineAttributedString;
|
|
7
|
+
|
|
8
|
+
NSMutableParagraphStyle *getOrCreateParagraphStyle(NSMutableAttributedString *output, NSUInteger index);
|
|
9
|
+
void applyParagraphSpacing(NSMutableAttributedString *output, NSUInteger start, CGFloat marginBottom);
|
|
10
|
+
void applyBlockSpacing(NSMutableAttributedString *output, CGFloat marginBottom);
|
|
11
|
+
void applyLineHeight(NSMutableAttributedString *output, NSRange range, CGFloat lineHeight);
|
|
12
|
+
|
|
13
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,56 @@
|
|
|
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 applyParagraphSpacing(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 applyBlockSpacing(NSMutableAttributedString *output, CGFloat marginBottom)
|
|
34
|
+
{
|
|
35
|
+
NSUInteger spacerLocation = output.length;
|
|
36
|
+
[output appendAttributedString:kNewlineAttributedString];
|
|
37
|
+
|
|
38
|
+
NSMutableParagraphStyle *spacerStyle = [kBlockSpacerTemplate mutableCopy];
|
|
39
|
+
spacerStyle.paragraphSpacing = marginBottom;
|
|
40
|
+
|
|
41
|
+
[output addAttribute:NSParagraphStyleAttributeName value:spacerStyle range:NSMakeRange(spacerLocation, 1)];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
void applyLineHeight(NSMutableAttributedString *output, NSRange range, CGFloat lineHeight)
|
|
45
|
+
{
|
|
46
|
+
if (lineHeight <= 0) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
NSMutableParagraphStyle *style = getOrCreateParagraphStyle(output, range.location);
|
|
51
|
+
|
|
52
|
+
style.minimumLineHeight = lineHeight;
|
|
53
|
+
style.maximumLineHeight = lineHeight;
|
|
54
|
+
|
|
55
|
+
[output addAttribute:NSParagraphStyleAttributeName value:style range:range];
|
|
56
|
+
}
|
|
@@ -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 ImageAttachments 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
|