Add formatting to NSAttributedString initilalized from Markdown
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// PopRichString+Markdown.m | |
// Pop | |
// | |
// Created by Nicholas Moore on 29/11/2022. | |
// | |
#import "PopRichString+Markdown.h" | |
#import "NMKit.h" | |
@implementation PopRichString (Markdown) | |
// add formatting to a NSAttributedString that has been constructed from Markdown | |
+ (NSAttributedString *)addFormattingToString:(NSAttributedString *)attrString withBaseFont:(NSFont *)baseFont paragraphSpacing:(BOOL)paraSpace | |
{ | |
NMLogTemp(@"AddFormattingWithBaseFont:\n%@", attrString); | |
if (![attrString isKindOfClass:[NSAttributedString class]]||![baseFont isKindOfClass:[NSFont class]]) { | |
return nil; | |
} | |
const NSRange fullRange=NSMakeRange(0, attrString.length); | |
// default paragraph style | |
NSParagraphStyle *const defaultParagraphStyle = [[NSParagraphStyle alloc] init]; | |
// indenting unit to match the default tab stops | |
const CGFloat unit=defaultParagraphStyle.tabStops[0].location; | |
// the bullet indenting is based on how TextEdit does it | |
const CGFloat bulletUnit=unit*0.3; | |
// paragraph styles | |
NSParagraphStyle *const listItemParagraphStyle = defaultParagraphStyle.copy; | |
NSParagraphStyle *const headerParagraphStyle = defaultParagraphStyle.copy; | |
NSMutableParagraphStyle *const indentedBlockParagraphStyle = defaultParagraphStyle.mutableCopy; | |
indentedBlockParagraphStyle.headIndent = unit; | |
indentedBlockParagraphStyle.firstLineHeadIndent = unit; | |
NSParagraphStyle *const codeBlockParagraphStyle = indentedBlockParagraphStyle.copy; | |
NSParagraphStyle *const quoteBlockParagraphStyle = indentedBlockParagraphStyle.copy; | |
if (@available(macOS 12.0, *)) { | |
NSMutableAttributedString *mas=[attrString mutableCopy]; | |
[mas addAttribute:NSFontAttributeName value:baseFont range:fullRange]; | |
// inline intents | |
[attrString enumerateAttribute:NSInlinePresentationIntentAttributeName inRange:fullRange options:0 usingBlock:^(NSNumber * _Nullable intentObj, NSRange range, BOOL * _Nonnull stop) { | |
NSUInteger intent = [intentObj unsignedIntegerValue]; | |
if (intent&NSInlinePresentationIntentEmphasized) { | |
NSFont *newFont = [NSFont fontWithDescriptor:[baseFont.fontDescriptor fontDescriptorWithSymbolicTraits:NSFontItalicTrait] size:baseFont.pointSize]; | |
[mas addAttribute:NSFontAttributeName value:newFont range:range]; | |
} | |
if (intent&NSInlinePresentationIntentStronglyEmphasized) { | |
NSFont *newFont = [NSFont fontWithDescriptor:[baseFont.fontDescriptor fontDescriptorWithSymbolicTraits:NSFontBoldTrait] size:baseFont.pointSize]; | |
[mas addAttribute:NSFontAttributeName value:newFont range:range]; | |
} | |
if (intent&NSInlinePresentationIntentStrikethrough) { | |
[mas addAttribute:NSStrikethroughStyleAttributeName value:@(NSUnderlineStyleSingle) range:range]; | |
} | |
if (intent&NSInlinePresentationIntentCode) { | |
NSFont *newFont = [NSFont monospacedSystemFontOfSize:baseFont.pointSize weight:NSFontWeightRegular]; | |
[mas addAttribute:NSFontAttributeName value:newFont range:range]; | |
} | |
}]; | |
//TODO | |
//NSInlinePresentationIntentSoftBreak | |
//NSInlinePresentationIntentLineBreak | |
//NSInlinePresentationIntentInlineHTML | |
//NSInlinePresentationIntentBlockHTML | |
// block intents | |
__block NSInteger previousListId=0; | |
__block BOOL lastBlock=YES; | |
__block BOOL listItem=NO; | |
// We use .reversed() iteration to be able to add characters to the string without breaking ranges | |
NSMutableDictionary *textListsLookup=[NSMutableDictionary dictionary]; | |
[attrString enumerateAttribute:NSPresentationIntentAttributeName inRange:NSMakeRange(0,attrString.length) options:NSAttributedStringEnumerationReverse usingBlock:^(NSPresentationIntent * _Nullable intent, const NSRange inRange, BOOL * _Nonnull stop) { | |
NMLogTemp(@"intent: %@", intent); | |
NSRange outRange=inRange; | |
BOOL listItemFollows=listItem; | |
listItem=NO; | |
BOOL firstItem=NO, lastItem=NO, codeBlock=NO; | |
NSMutableParagraphStyle *paragraphStyle=defaultParagraphStyle.mutableCopy; | |
NSMutableArray *textLists=[NSMutableArray array]; | |
do { | |
switch (intent.intentKind) { | |
case NSPresentationIntentKindParagraph: | |
break; | |
case NSPresentationIntentKindHeader: | |
{ | |
NSFont *newFont=baseFont; | |
switch (intent.headerLevel) { | |
case 1: | |
newFont = [NSFont fontWithDescriptor:[baseFont.fontDescriptor fontDescriptorWithSymbolicTraits:NSFontBoldTrait] size:baseFont.pointSize*1.8]; | |
break; | |
case 2: | |
newFont = [NSFont fontWithDescriptor:[baseFont.fontDescriptor fontDescriptorWithSymbolicTraits:NSFontBoldTrait] size:baseFont.pointSize*1.5]; | |
break; | |
case 3: | |
newFont = [NSFont fontWithDescriptor:[baseFont.fontDescriptor fontDescriptorWithSymbolicTraits:NSFontBoldTrait] size:baseFont.pointSize*1.2]; | |
break; | |
default: | |
newFont = [NSFont fontWithDescriptor:[baseFont.fontDescriptor fontDescriptorWithSymbolicTraits:NSFontBoldTrait] size:baseFont.pointSize*1.0]; | |
break; | |
} | |
[mas addAttribute:NSFontAttributeName value:newFont range:outRange]; | |
paragraphStyle=headerParagraphStyle.mutableCopy; | |
} | |
break; | |
case NSPresentationIntentKindOrderedList: | |
case NSPresentationIntentKindUnorderedList: | |
NMLogTemp(@"LIST"); | |
if (intent.indentationLevel==0) { | |
lastItem = intent.identity!=previousListId; | |
previousListId=intent.identity; | |
} | |
break; | |
case NSPresentationIntentKindListItem: | |
NMLogTemp(@"LIST ITEM"); | |
if (!firstItem) { | |
firstItem=intent.ordinal==1&&intent.indentationLevel==1; | |
} | |
if (intent.parentIntent) { | |
NSNumber *parentId=@(intent.parentIntent.identity); | |
NSTextList *list=textListsLookup[parentId]; | |
if (!list) { | |
NSTextListMarkerFormat markerFormat=NSTextListMarkerHyphen; | |
if (intent.parentIntent.intentKind == NSPresentationIntentKindUnorderedList) { | |
NSUInteger markerIndex=MIN(intent.parentIntent.indentationLevel,2); | |
NMLogTemp(@"MARKER INDEX %@", @(markerIndex)); | |
markerFormat=@[NSTextListMarkerDisc, NSTextListMarkerCircle, NSTextListMarkerSquare][markerIndex]; | |
} else if (intent.parentIntent.intentKind == NSPresentationIntentKindOrderedList) { | |
markerFormat=NSTextListMarkerDecimal; | |
} | |
list=[[NSTextList alloc] initWithMarkerFormat:markerFormat options:0]; | |
textListsLookup[parentId]=list; | |
} | |
[textLists insertObject:list atIndex:0]; | |
// insert actual bullet | |
if (!listItem) { | |
paragraphStyle=listItemParagraphStyle.mutableCopy; | |
NSString *bullet=[NSString stringWithFormat:@"\t%@\t", [list markerForItemNumber:intent.ordinal]]; | |
NMLogTemp(@"BULLET IS %@", bullet); | |
[mas insertAttributedString:[[NSAttributedString alloc] initWithString:bullet attributes:@{NSFontAttributeName: baseFont}] atIndex:outRange.location]; | |
outRange.length+=bullet.length; | |
paragraphStyle.tabStops=@[ | |
[[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentLeft location:unit*intent.indentationLevel options:@{}], | |
[[NSTextTab alloc] initWithTextAlignment:NSTextAlignmentLeft location:unit*(intent.indentationLevel-1)+bulletUnit options:@{}], | |
]; | |
paragraphStyle.headIndent=unit*intent.indentationLevel; | |
} | |
paragraphStyle.textLists=textLists; | |
listItem=YES; | |
} | |
break; | |
case NSPresentationIntentKindCodeBlock: | |
[mas addAttribute:NSFontAttributeName value:[NSFont monospacedSystemFontOfSize:baseFont.pointSize weight:NSFontWeightRegular] range:outRange]; | |
paragraphStyle = codeBlockParagraphStyle.mutableCopy; | |
codeBlock=YES; | |
break; | |
case NSPresentationIntentKindBlockQuote: | |
paragraphStyle = quoteBlockParagraphStyle.mutableCopy; | |
break; | |
default: | |
break; | |
} | |
intent = intent.parentIntent; | |
} while(intent); | |
NMLogTemp(@"TEXT LISTS %@", textLists); | |
//TODO | |
//NSPresentationIntentKindThematicBreak, | |
//NSPresentationIntentKindTable, | |
//NSPresentationIntentKindTableHeaderRow, | |
//NSPresentationIntentKindTableRow, | |
//NSPresentationIntentKindTableCell, | |
NMLogTemp(@"spacing: %@ / listItem %@ firstitem %@ lastItem %@", NMLimitString([attrString attributedSubstringFromRange:inRange].string, 10, nil), | |
@(listItem), @(firstItem), @(lastItem)); | |
NSMutableString *newlineAppend=[@"" mutableCopy]; | |
// add spacing for paragraphs | |
if (!codeBlock) { | |
[newlineAppend appendString:@"\n"]; | |
} | |
if (!listItem&&!listItemFollows) { | |
if (paraSpace) { | |
paragraphStyle.paragraphSpacing = baseFont.pointSize; | |
} | |
else if (!lastBlock) { | |
[newlineAppend appendString:@"\n"]; | |
} | |
} | |
[mas insertAttributedString:[[NSAttributedString alloc] initWithString:newlineAppend attributes:@{NSFontAttributeName: baseFont}] atIndex:outRange.location+outRange.length]; | |
outRange.length+=newlineAppend.length; | |
[mas addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:outRange]; | |
lastBlock=NO; | |
}]; | |
NMLogTemp(@"Done AddFormattingWithBaseFont:\n%@", mas); | |
return [mas copy]; | |
} | |
else { | |
return nil; | |
} | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi @pilotmoon
Is it working on iOS? I've never got any
intentKind
exceptNSPresentationIntentKindHeader
orNSPresentationIntentKindHeader