Skip to content

Instantly share code, notes, and snippets.

@pilotmoon
Created January 21, 2023 17:41
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pilotmoon/a67510394abb51118ba00e69347ee7dd to your computer and use it in GitHub Desktop.
Save pilotmoon/a67510394abb51118ba00e69347ee7dd to your computer and use it in GitHub Desktop.
Add formatting to NSAttributedString initilalized from Markdown
//
// 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
@snyuryev
Copy link

snyuryev commented Jul 7, 2023

Hi @pilotmoon
Is it working on iOS? I've never got any intentKind except NSPresentationIntentKindHeader or NSPresentationIntentKindHeader

@pilotmoon
Copy link
Author

I have only tested it on Mac

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment