diff --git a/.swift-version b/.swift-version new file mode 100644 index 0000000..bb576db --- /dev/null +++ b/.swift-version @@ -0,0 +1 @@ +2.3 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..4c143d5 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,14 @@ +# references: +# * http://www.objc.io/issue-6/travis-ci.html +# * https://github.com/supermarin/xcpretty#usage + +osx_image: xcode7.3 +language: objective-c +# cache: cocoapods +# podfile: Example/Podfile +# before_install: +# - gem install cocoapods # Since Travis is not always on latest version +# - pod install --project-directory=Example +script: +- set -o pipefail && xcodebuild test -enableCodeCoverage YES -workspace Example/MarkDownEditor.xcworkspace -scheme MarkDownEditor-Example -sdk iphonesimulator9.3 ONLY_ACTIVE_ARCH=NO | xcpretty +- pod lib lint diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5470681 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2017 Cu-Toof + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/MarkDownEditor.podspec b/MarkDownEditor.podspec new file mode 100644 index 0000000..3ec1ed7 --- /dev/null +++ b/MarkDownEditor.podspec @@ -0,0 +1,43 @@ +# +# Be sure to run `pod lib lint MarkDownEditor.podspec' to ensure this is a +# valid spec before submitting. +# +# Any lines starting with a # are optional, but their use is encouraged +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html +# + +Pod::Spec.new do |s| + s.name = 'MarkDownEditor' + s.version = '1.0.0' + s.summary = 'Read MarkDown File.' + +# This description is used to generate tags and improve search results. +# * Think: What does it do? Why did you write it? What is the focus? +# * Try to keep it short, snappy and to the point. +# * Write the description between the DESC delimiters below. +# * Finally, don't worry about the indent, CocoaPods strips it! + + s.description = <<-DESC +MarkDownEditor help you read MarkDown file in iOS. + DESC + + s.homepage = 'https://github.com/Cu-Toof/MarkDownEditor' + # s.screenshots = 'www.example.com/screenshots_1', 'www.example.com/screenshots_2' + s.license = { :type => 'MIT', :file => 'LICENSE' } + s.author = { 'Cu-Toof' => 'toanf9dn@gmail.com' } + s.source = { :git => 'https://github.com/Cu-Toof/MarkDownEditor.git', :tag => s.version.to_s } + # s.social_media_url = 'https://twitter.com/' + + s.ios.deployment_target = '8.0' + + s.source_files = 'MarkDownEditor/Classes/**/*' + + # s.resource_bundles = { + # 'MarkDownEditor' => ['MarkDownEditor/Assets/*.png'] + # } + + # s.public_header_files = 'Pod/Classes/**/*.h' + s.frameworks = 'UIKit' + s.dependency 'cmark', '~> 0.21.0' + s.dependency 'Ono', '~> 1.1.3' +end diff --git a/MarkDownEditor/Assets/.gitkeep b/MarkDownEditor/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/MarkDownEditor/Classes/.gitkeep b/MarkDownEditor/Classes/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/MarkDownEditor/Classes/CMAttributeRun.h b/MarkDownEditor/Classes/CMAttributeRun.h new file mode 100755 index 0000000..789d490 --- /dev/null +++ b/MarkDownEditor/Classes/CMAttributeRun.h @@ -0,0 +1,26 @@ +// +// CMAttributeRun.h +// MarkDownEditor +// +// Created by Indragie on 1/15/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import +#import "CMPlatformDefines.h" + +@interface CMAttributeRun : NSObject +@property (nonatomic, readonly) NSDictionary *attributes; +@property (nonatomic, readonly) CMFontSymbolicTraits fontTraits; +@property (nonatomic) NSInteger orderedListItemNumber; +@property (nonatomic, readonly) BOOL listTight; + +- (instancetype)initWithAttributes:(NSDictionary *)attributes + fontTraits:(CMFontSymbolicTraits)fontTraits + orderedListNumber:(NSInteger)orderedListNumber; + +@end + +CMAttributeRun * CMDefaultAttributeRun(NSDictionary *attributes); +CMAttributeRun * CMTraitAttributeRun(NSDictionary *attributes, CMFontSymbolicTraits traits); +CMAttributeRun * CMOrderedListAttributeRun(NSDictionary *attributes, NSInteger startingNumber); diff --git a/MarkDownEditor/Classes/CMAttributeRun.m b/MarkDownEditor/Classes/CMAttributeRun.m new file mode 100755 index 0000000..12c70ae --- /dev/null +++ b/MarkDownEditor/Classes/CMAttributeRun.m @@ -0,0 +1,40 @@ +// +// CMAttributeRun.m +// MarkDownEditor +// +// Created by Indragie on 1/15/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMAttributeRun.h" + +CMAttributeRun * CMDefaultAttributeRun(NSDictionary *attributes) +{ + return [[CMAttributeRun alloc] initWithAttributes:attributes fontTraits:0 orderedListNumber:0]; +} + +CMAttributeRun * CMTraitAttributeRun(NSDictionary *attributes, CMFontSymbolicTraits traits) +{ + return [[CMAttributeRun alloc] initWithAttributes:attributes fontTraits:traits orderedListNumber:0]; +} + +CMAttributeRun * CMOrderedListAttributeRun(NSDictionary *attributes, NSInteger startingNumber) +{ + return [[CMAttributeRun alloc] initWithAttributes:attributes fontTraits:0 orderedListNumber:startingNumber]; +} + +@implementation CMAttributeRun + +- (instancetype)initWithAttributes:(NSDictionary *)attributes + fontTraits:(CMFontSymbolicTraits)fontTraits + orderedListNumber:(NSInteger)orderedListNumber +{ + if ((self = [super init])) { + _attributes = attributes; + _fontTraits = fontTraits; + _orderedListItemNumber = orderedListNumber; + } + return self; +} + +@end diff --git a/MarkDownEditor/Classes/CMAttributedStringRenderer.h b/MarkDownEditor/Classes/CMAttributedStringRenderer.h new file mode 100755 index 0000000..8ed9fe6 --- /dev/null +++ b/MarkDownEditor/Classes/CMAttributedStringRenderer.h @@ -0,0 +1,47 @@ +// +// CMAttributedStringRenderer.h +// MarkDownEditor +// +// Created by Indragie on 1/14/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import + +@class CMDocument; +@class CMTextAttributes; +@protocol CMHTMLElementTransformer; +/** + * Renders an attributed string from a Markdown document + */ +@interface CMAttributedStringRenderer : NSObject + +/** + * Designated initializer. + * + * @param document A Markdown document. + * @param attributes Attributes used to style the string. + * + * @return An initialized instance of the receiver. + */ +- (instancetype)initWithDocument:(CMDocument *)document attributes:(CMTextAttributes *)attributes; + +/** + * Registers a handler to transform HTML elements. + * + * Only a single transformer can be registered for an element. If a transformer + * is already registered for an element, it will be replaced. + * + * @param transformer The transformer to register. + */ +- (void)registerHTMLElementTransformer:(id)transformer; + +/** + * Renders an attributed string from the Markdown document. + * + * @return An attributed string containing the contents of the Markdown document, + * styled using the attributes set on the receiver. + */ +- (NSAttributedString *)render; + +@end diff --git a/MarkDownEditor/Classes/CMAttributedStringRenderer.m b/MarkDownEditor/Classes/CMAttributedStringRenderer.m new file mode 100755 index 0000000..f005752 --- /dev/null +++ b/MarkDownEditor/Classes/CMAttributedStringRenderer.m @@ -0,0 +1,373 @@ +// +// CMAttributedStringRenderer.m +// MarkDownEditor +// +// Created by Indragie on 1/14/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMAttributedStringRenderer.h" +#import "CMAttributeRun.h" +#import "CMCascadingAttributeStack.h" +#import "CMStack.h" +#import "CMHTMLElementTransformer.h" +#import "CMHTMLElement.h" +#import "CMHTMLUtilities.h" +#import "CMTextAttributes.h" +#import "CMNode.h" +#import "CMParser.h" + +#import "Ono.h" + +@interface CMAttributedStringRenderer () +@end + +@implementation CMAttributedStringRenderer { + CMDocument *_document; + CMTextAttributes *_attributes; + CMCascadingAttributeStack *_attributeStack; + CMStack *_HTMLStack; + NSMutableDictionary *_tagNameToTransformerMapping; + NSMutableAttributedString *_buffer; + NSAttributedString *_attributedString; +} + +- (instancetype)initWithDocument:(CMDocument *)document attributes:(CMTextAttributes *)attributes +{ + if ((self = [super init])) { + _document = document; + _attributes = attributes; + _tagNameToTransformerMapping = [[NSMutableDictionary alloc] init]; + } + return self; +} + +- (void)registerHTMLElementTransformer:(id)transformer +{ + NSParameterAssert(transformer); + _tagNameToTransformerMapping[[transformer.class tagName]] = transformer; +} + +- (NSAttributedString *)render +{ + if (_attributedString == nil) { + _attributeStack = [[CMCascadingAttributeStack alloc] init]; + _HTMLStack = [[CMStack alloc] init]; + _buffer = [[NSMutableAttributedString alloc] init]; + + CMParser *parser = [[CMParser alloc] initWithDocument:_document delegate:self]; + [parser parse]; + + _attributedString = [_buffer copy]; + _attributeStack = nil; + _HTMLStack = nil; + _buffer = nil; + } + + return _attributedString; +} + +#pragma mark - CMParserDelegate + +- (void)parserDidStartDocument:(CMParser *)parser +{ + [_attributeStack push:CMDefaultAttributeRun(_attributes.textAttributes)]; +} + +- (void)parserDidEndDocument:(CMParser *)parser +{ + CFStringTrimWhitespace((__bridge CFMutableStringRef)_buffer.mutableString); +} + +- (void)parser:(CMParser *)parser foundText:(NSString *)text +{ + CMHTMLElement *element = [_HTMLStack peek]; + if (element != nil) { + [element.buffer appendString:text]; + } else { + [self appendString:text]; + } +} + +- (void)parser:(CMParser *)parser didStartHeaderWithLevel:(NSInteger)level +{ + [_attributeStack push:CMDefaultAttributeRun([_attributes attributesForHeaderLevel:level])]; +} + +- (void)parser:(CMParser *)parser didEndHeaderWithLevel:(NSInteger)level +{ + [self appendString:@"\n"]; + [_attributeStack pop]; +} + +- (void)parserDidStartParagraph:(CMParser *)parser +{ + if (![self nodeIsInTightMode:parser.currentNode]) { + NSMutableParagraphStyle* paragraphStyle = [NSMutableParagraphStyle new]; + paragraphStyle.paragraphSpacingBefore = 12; + + [_attributeStack push:CMDefaultAttributeRun(@{NSParagraphStyleAttributeName: paragraphStyle})]; + } +} + +- (void)parserDidEndParagraph:(CMParser *)parser +{ + if (![self nodeIsInTightMode:parser.currentNode]) { + [_attributeStack pop]; + [self appendString:@"\n"]; + } +} + +- (void)parserDidStartEmphasis:(CMParser *)parser +{ + BOOL hasExplicitFont = _attributes.emphasisAttributes[NSFontAttributeName] != nil; + [_attributeStack push:CMTraitAttributeRun(_attributes.emphasisAttributes, hasExplicitFont ? 0 : CMFontTraitItalic)]; +} + +- (void)parserDidEndEmphasis:(CMParser *)parser +{ + [_attributeStack pop]; +} + +- (void)parserDidStartStrong:(CMParser *)parser +{ + BOOL hasExplicitFont = _attributes.strongAttributes[NSFontAttributeName] != nil; + [_attributeStack push:CMTraitAttributeRun(_attributes.strongAttributes, hasExplicitFont ? 0 : CMFontTraitBold)]; +} + +- (void)parserDidEndStrong:(CMParser *)parse +{ + [_attributeStack pop]; +} + +- (void)parser:(CMParser *)parser didStartLinkWithURL:(NSURL *)URL title:(NSString *)title +{ + NSMutableDictionary *baseAttributes = [NSMutableDictionary dictionaryWithObjectsAndKeys:URL, NSLinkAttributeName, nil]; +#if !TARGET_OS_IPHONE + if (title != nil) { + baseAttributes[NSToolTipAttributeName] = title; + } +#endif + [baseAttributes addEntriesFromDictionary:_attributes.linkAttributes]; + [_attributeStack push:CMDefaultAttributeRun(baseAttributes)]; +} + +- (void)parser:(CMParser *)parser didEndLinkWithURL:(NSURL *)URL title:(NSString *)title +{ + [_attributeStack pop]; +} + +- (void)parser:(CMParser *)parser foundHTML:(NSString *)HTML +{ + NSString *tagName = CMTagNameFromHTMLTag(HTML); + if (tagName.length != 0) { + CMHTMLElement *element = [self newHTMLElementForTagName:tagName HTML:HTML]; + if (element != nil) { + [self appendHTMLElement:element]; + } + } +} + +- (void)parser:(CMParser *)parser foundInlineHTML:(NSString *)HTML +{ + NSString *tagName = CMTagNameFromHTMLTag(HTML); + if (tagName.length != 0) { + CMHTMLElement *element = nil; + if (CMIsHTMLVoidTagName(tagName)) { + element = [self newHTMLElementForTagName:tagName HTML:HTML]; + if (element != nil) { + [self appendHTMLElement:element]; + } + } else if (CMIsHTMLClosingTag(HTML)) { + if ((element = [_HTMLStack pop])) { + NSAssert([element.tagName isEqualToString:tagName], @"Closing tag does not match opening tag"); + [element.buffer appendString:HTML]; + [self appendHTMLElement:element]; + } + } else if (CMIsHTMLTag(HTML)) { + element = [self newHTMLElementForTagName:tagName HTML:HTML]; + if (element != nil) { + [_HTMLStack push:element]; + } + } + } +} + +- (void)parser:(CMParser *)parser foundCodeBlock:(NSString *)code info:(NSString *)info +{ + [_attributeStack push:CMDefaultAttributeRun(_attributes.codeBlockAttributes)]; + [self appendString:[NSString stringWithFormat:@"\n\n%@\n\n", code]]; + [_attributeStack pop]; +} + +- (void)parser:(CMParser *)parser foundInlineCode:(NSString *)code +{ + [_attributeStack push:CMDefaultAttributeRun(_attributes.inlineCodeAttributes)]; + [self appendString:code]; + [_attributeStack pop]; +} + +- (void)parserFoundSoftBreak:(CMParser *)parser +{ + [self appendString:@" "]; +} + +- (void)parserFoundLineBreak:(CMParser *)parser +{ + [self appendString:@"\n"]; +} + +- (void)parserDidStartBlockQuote:(CMParser *)parser +{ + [_attributeStack push:CMDefaultAttributeRun(_attributes.blockQuoteAttributes)]; +} + +- (void)parserDidEndBlockQuote:(CMParser *)parser +{ + [_attributeStack pop]; +} + +- (void)parser:(CMParser *)parser didStartUnorderedListWithTightness:(BOOL)tight +{ + [_attributeStack push:CMDefaultAttributeRun([self listAttributesForNode:parser.currentNode])]; + [self appendString:@"\n"]; +} + +- (void)parser:(CMParser *)parser didEndUnorderedListWithTightness:(BOOL)tight +{ + [_attributeStack pop]; +} + +- (void)parser:(CMParser *)parser didStartOrderedListWithStartingNumber:(NSInteger)num tight:(BOOL)tight +{ + [_attributeStack push:CMOrderedListAttributeRun([self listAttributesForNode:parser.currentNode], num)]; + [self appendString:@"\n"]; +} + +- (void)parser:(CMParser *)parser didEndOrderedListWithStartingNumber:(NSInteger)num tight:(BOOL)tight +{ + [_attributeStack pop]; +} + +- (void)parserDidStartListItem:(CMParser *)parser +{ + CMNode *node = parser.currentNode.parent; + switch (node.listType) { + case CMListTypeNone: + NSAssert(NO, @"Parent node of list item must be a list"); + break; + case CMListTypeUnordered: { + [self appendString:@"\u2022 "]; + [_attributeStack push:CMDefaultAttributeRun(_attributes.unorderedListItemAttributes)]; + break; + } + case CMListTypeOrdered: { + CMAttributeRun *parentRun = [_attributeStack peek]; + [self appendString:[NSString stringWithFormat:@"%ld. ", (long)parentRun.orderedListItemNumber]]; + parentRun.orderedListItemNumber++; + [_attributeStack push:CMDefaultAttributeRun(_attributes.orderedListItemAttributes)]; + break; + } + default: + break; + } +} + +- (void)parserDidEndListItem:(CMParser *)parser +{ + if (parser.currentNode.next != nil || [self sublistLevel:parser.currentNode] == 1) { + [self appendString:@"\n"]; + } + [_attributeStack pop]; +} + +#pragma mark - Private + +- (NSDictionary *)listAttributesForNode:(CMNode *)node +{ + if (node.listType == CMListTypeNone) { + return nil; + } + + NSUInteger sublistLevel = [self sublistLevel:node.parent]; + if (sublistLevel == 0) { + return node.listType == CMListTypeOrdered ? _attributes.orderedListAttributes : _attributes.unorderedListAttributes; + } + + NSParagraphStyle *rootListParagraphStyle = [NSParagraphStyle defaultParagraphStyle]; + NSMutableDictionary *listAttributes; + if (node.listType == CMListTypeOrdered) { + listAttributes = [_attributes.orderedSublistAttributes mutableCopy]; + rootListParagraphStyle = _attributes.orderedListAttributes[NSParagraphStyleAttributeName]; + } else { + listAttributes = [_attributes.unorderedSublistAttributes mutableCopy]; + rootListParagraphStyle = _attributes.unorderedListAttributes[NSParagraphStyleAttributeName]; + } + + if (listAttributes[NSParagraphStyleAttributeName] != nil) { + NSMutableParagraphStyle *paragraphStyle = [((NSParagraphStyle *)listAttributes[NSParagraphStyleAttributeName]) mutableCopy]; + paragraphStyle.headIndent = rootListParagraphStyle.headIndent + paragraphStyle.headIndent * sublistLevel; + paragraphStyle.firstLineHeadIndent = rootListParagraphStyle.firstLineHeadIndent + paragraphStyle.firstLineHeadIndent * sublistLevel; + listAttributes[NSParagraphStyleAttributeName] = paragraphStyle; + } + + return [listAttributes copy]; +} + +- (NSUInteger)sublistLevel:(CMNode *)node +{ + if (node.parent == nil) { + return 0; + } else { + return (node.listType == CMListTypeNone ? 0 : 1) + [self sublistLevel:node.parent]; + } +} + +- (CMHTMLElement *)newHTMLElementForTagName:(NSString *)tagName HTML:(NSString *)HTML +{ + NSParameterAssert(tagName); + id transformer = _tagNameToTransformerMapping[tagName]; + if (transformer != nil) { + CMHTMLElement *element = [[CMHTMLElement alloc] initWithTransformer:transformer]; + [element.buffer appendString:HTML]; + return element; + } + return nil; +} + +- (BOOL)nodeIsInTightMode:(CMNode *)node +{ + CMNode *grandparent = node.parent.parent; + return grandparent.listTight; +} + +- (void)appendString:(NSString *)string +{ + NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:_attributeStack.cascadedAttributes]; + [_buffer appendAttributedString:attrString]; +} + +- (void)appendHTMLElement:(CMHTMLElement *)element +{ + NSError *error = nil; + ONOXMLDocument *document = [ONOXMLDocument HTMLDocumentWithString:element.buffer encoding:NSUTF8StringEncoding error:&error]; + if (document == nil) { + NSLog(@"Error creating HTML document for buffer \"%@\": %@", element.buffer, error); + return; + } + + ONOXMLElement *XMLElement = document.rootElement[0][0]; + NSDictionary *attributes = _attributeStack.cascadedAttributes; + NSAttributedString *attrString = [element.transformer attributedStringForElement:XMLElement attributes:attributes]; + + if (attrString != nil) { + CMHTMLElement *parentElement = [_HTMLStack peek]; + if (parentElement == nil) { + [_buffer appendAttributedString:attrString]; + } else { + [parentElement.buffer appendString:attrString.string]; + } + } +} + +@end diff --git a/MarkDownEditor/Classes/CMCascadingAttributeStack.h b/MarkDownEditor/Classes/CMCascadingAttributeStack.h new file mode 100755 index 0000000..396fddc --- /dev/null +++ b/MarkDownEditor/Classes/CMCascadingAttributeStack.h @@ -0,0 +1,20 @@ +// +// CMCascadingAttributeStack.h +// MarkDownEditor +// +// Created by Indragie on 1/15/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import + +@class CMAttributeRun; + +@interface CMCascadingAttributeStack : NSObject +@property (nonatomic, readonly) NSDictionary *cascadedAttributes; + +- (void)push:(CMAttributeRun *)run; +- (CMAttributeRun *)pop; +- (CMAttributeRun *)peek; + +@end diff --git a/MarkDownEditor/Classes/CMCascadingAttributeStack.m b/MarkDownEditor/Classes/CMCascadingAttributeStack.m new file mode 100755 index 0000000..ec8f884 --- /dev/null +++ b/MarkDownEditor/Classes/CMCascadingAttributeStack.m @@ -0,0 +1,85 @@ +// +// CMCascadingAttributeStack.m +// MarkDownEditor +// +// Created by Indragie on 1/15/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMCascadingAttributeStack.h" +#import "CMAttributeRun.h" +#import "CMPlatformDefines.h" +#import "CMStack.h" + +static CMFont * CMFontWithTraits(CMFontSymbolicTraits traits, CMFont *font) +{ + CMFontSymbolicTraits combinedTraits = font.fontDescriptor.symbolicTraits | (traits & 0xFFFF); +#if TARGET_OS_IPHONE + UIFontDescriptor *descriptor = [font.fontDescriptor fontDescriptorWithSymbolicTraits:combinedTraits]; + return [UIFont fontWithDescriptor:descriptor size:font.pointSize]; +#else + NSDictionary *attributes = @{ + NSFontFamilyAttribute: font.familyName, + NSFontTraitsAttribute: @{ + NSFontSymbolicTrait: @(combinedTraits) + }, + }; + NSFontDescriptor *descriptor = [NSFontDescriptor fontDescriptorWithFontAttributes:attributes]; + return [NSFont fontWithDescriptor:descriptor size:font.pointSize]; +#endif +} + +@implementation CMCascadingAttributeStack { + CMStack *_stack; + NSDictionary *_cascadedAttributes; +} + +- (instancetype)init +{ + if ((self = [super init])) { + _stack = [[CMStack alloc] init]; + } + return self; +} + +- (void)push:(CMAttributeRun *)run +{ + [_stack push:run]; + _cascadedAttributes = nil; +} + +- (CMAttributeRun *)pop +{ + _cascadedAttributes = nil; + return [_stack pop]; +} + +- (CMAttributeRun *)peek +{ + return [_stack peek]; +} + +- (NSDictionary *)cascadedAttributes +{ + if (_cascadedAttributes == nil) { + NSMutableDictionary *allAttributes = [[NSMutableDictionary alloc] init]; + for (CMAttributeRun *run in _stack.objects) { + CMFont *baseFont; + CMFont *adjustedFont; + if (run.fontTraits != 0 && + (baseFont = allAttributes[NSFontAttributeName]) && + (adjustedFont = CMFontWithTraits(run.fontTraits, baseFont))) { + + NSMutableDictionary *attributes = [NSMutableDictionary dictionaryWithDictionary:run.attributes]; + attributes[NSFontAttributeName] = adjustedFont; + [allAttributes addEntriesFromDictionary:attributes]; + } else if (run.attributes != nil) { + [allAttributes addEntriesFromDictionary:run.attributes]; + } + } + _cascadedAttributes = allAttributes; + } + return _cascadedAttributes; +} + +@end diff --git a/MarkDownEditor/Classes/CMDocument+AttributedStringAdditions.h b/MarkDownEditor/Classes/CMDocument+AttributedStringAdditions.h new file mode 100755 index 0000000..66811e1 --- /dev/null +++ b/MarkDownEditor/Classes/CMDocument+AttributedStringAdditions.h @@ -0,0 +1,25 @@ +// +// CMDocument+AttributedStringAdditions.h +// MarkDownEditor +// +// Created by Indragie on 1/20/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMDocument.h" + +@class CMTextAttributes; + +@interface CMDocument (AttributedStringAdditions) + +/** + * Creates an attributed string from the receiver that is styled using the + * specified attributes. + * + * @param attributes Attributes used to style the text. + * + * @return Attributed string containing the formatted contents of the receiver. + */ +- (NSAttributedString *)attributedStringWithAttributes:(CMTextAttributes *)attributes; + +@end diff --git a/MarkDownEditor/Classes/CMDocument+AttributedStringAdditions.m b/MarkDownEditor/Classes/CMDocument+AttributedStringAdditions.m new file mode 100755 index 0000000..b5a1b2d --- /dev/null +++ b/MarkDownEditor/Classes/CMDocument+AttributedStringAdditions.m @@ -0,0 +1,20 @@ +// +// CMDocument+AttributedStringAdditions.m +// MarkDownEditor +// +// Created by Indragie on 1/20/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMDocument+AttributedStringAdditions.h" +#import "CMAttributedStringRenderer.h" + +@implementation CMDocument (AttributedStringAdditions) + +- (NSAttributedString *)attributedStringWithAttributes:(CMTextAttributes *)attributes +{ + CMAttributedStringRenderer *renderer = [[CMAttributedStringRenderer alloc] initWithDocument:self attributes:attributes]; + return [renderer render]; +} + +@end diff --git a/MarkDownEditor/Classes/CMDocument+HTMLAdditions.h b/MarkDownEditor/Classes/CMDocument+HTMLAdditions.h new file mode 100755 index 0000000..365b564 --- /dev/null +++ b/MarkDownEditor/Classes/CMDocument+HTMLAdditions.h @@ -0,0 +1,21 @@ +// +// CMDocument+HTMLAdditions.h +// MarkDownEditor +// +// Created by Indragie on 1/20/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMDocument.h" +#import "CMHTMLRenderer.h" + +@interface CMDocument (HTMLAdditions) + +/** + * Creates an HTML representation of the receiver. + * + * @return String containing the HTML representation of the receiver. + */ +- (NSString *)HTMLString; + +@end diff --git a/MarkDownEditor/Classes/CMDocument+HTMLAdditions.m b/MarkDownEditor/Classes/CMDocument+HTMLAdditions.m new file mode 100755 index 0000000..4b2aa1f --- /dev/null +++ b/MarkDownEditor/Classes/CMDocument+HTMLAdditions.m @@ -0,0 +1,19 @@ +// +// CMDocument+HTMLAdditions.m +// MarkDownEditor +// +// Created by Indragie on 1/20/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMDocument+HTMLAdditions.h" + +@implementation CMDocument (HTMLAdditions) + +- (NSString *)HTMLString +{ + CMHTMLRenderer *renderer = [[CMHTMLRenderer alloc] initWithDocument:self]; + return [renderer render]; +} + +@end diff --git a/MarkDownEditor/Classes/CMDocument.h b/MarkDownEditor/Classes/CMDocument.h new file mode 100755 index 0000000..61f099c --- /dev/null +++ b/MarkDownEditor/Classes/CMDocument.h @@ -0,0 +1,63 @@ +// +// CMDocument.h +// MarkDownEditor +// +// Created by Indragie on 1/12/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import + +@class CMNode; + +typedef NS_OPTIONS(NSInteger, CMDocumentOptions) { + /** + * Include a `data-sourcepos` attribute on all block elements. + */ + CMDocumentOptionsSourcepos = (1 << 0), + /** + * Render `softbreak` elements as hard line breaks. + */ + CMDocumentOptionsHardBreaks = (1 << 1), + /** + * Normalize tree by consolidating adjacent text nodes. + */ + CMDocumentOptionsNormalize = (1 << 3), + /** + * Convert straight quotes to curly, --- to em dashes, -- to en dashes. + */ + CMDocumentOptionsSmart = (1 << 4) +}; + +/** + * A Markdown document conforming to the CommonMark spec. + */ +@interface CMDocument : NSObject + +/** + * Root node of the document. + */ +@property (nonatomic, readonly) CMNode *rootNode; + +/** + * Initializes the receiver with data. + * + * @param data Markdown document data. + * @param options Document options. + * + * @return An initialized instance of the receiver. + */ +- (instancetype)initWithData:(NSData *)data options:(CMDocumentOptions)options; + +/** + * Initializes the receiver with data read from a file. + * + * @param path The file path to read from. + * @param options Document options. + * + * @return An initialized instance of the receiver, or `nil` if the file + * could not be opened. + */ +- (instancetype)initWithContentsOfFile:(NSString *)path options:(CMDocumentOptions)options; + +@end diff --git a/MarkDownEditor/Classes/CMDocument.m b/MarkDownEditor/Classes/CMDocument.m new file mode 100755 index 0000000..bc951b0 --- /dev/null +++ b/MarkDownEditor/Classes/CMDocument.m @@ -0,0 +1,44 @@ +// +// CMDocument.m +// MarkDownEditor +// +// Created by Indragie on 1/12/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMDocument_Private.h" +#import "CMNode_Private.h" + +@implementation CMDocument + +- (instancetype)initWithData:(NSData *)data options:(CMDocumentOptions)options +{ + NSParameterAssert(data); + + if ((self = [super init])) { + cmark_node *node = cmark_parse_document((const char *)data.bytes, data.length, options); + if (node == NULL) return nil; + + _rootNode = [[CMNode alloc] initWithNode:node freeWhenDone:YES]; + _options = options; + } + return self; +} + +- (instancetype)initWithContentsOfFile:(NSString *)path options:(CMDocumentOptions)options +{ + if ((self = [super init])) { + FILE *fp = fopen(path.UTF8String, "r"); + if (fp == NULL) return nil; + + cmark_node *node = cmark_parse_file(fp, options); + fclose(fp); + if (node == NULL) return nil; + + _rootNode = [[CMNode alloc] initWithNode:node freeWhenDone:YES]; + _options = options; + } + return self; +} + +@end diff --git a/MarkDownEditor/Classes/CMDocument_Private.h b/MarkDownEditor/Classes/CMDocument_Private.h new file mode 100755 index 0000000..f46c484 --- /dev/null +++ b/MarkDownEditor/Classes/CMDocument_Private.h @@ -0,0 +1,16 @@ +// +// CMDocument_Private.h +// MarkDownEditor +// +// Created by Indragie on 4/27/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMDocument.h" + +@interface CMDocument () +/** + * Options passed in at initialization time. + */ +@property (nonatomic, readonly) CMDocumentOptions options; +@end diff --git a/MarkDownEditor/Classes/CMHTMLElement.h b/MarkDownEditor/Classes/CMHTMLElement.h new file mode 100755 index 0000000..d5ea2d5 --- /dev/null +++ b/MarkDownEditor/Classes/CMHTMLElement.h @@ -0,0 +1,20 @@ +// +// CMHTMLElement.h +// MarkDownEditor +// +// Created by Indragie on 1/16/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import + +@protocol CMHTMLElementTransformer; + +@interface CMHTMLElement : NSObject +@property (nonatomic, readonly) id transformer; +@property (nonatomic, readonly) NSString *tagName; +@property (nonatomic, readonly) NSMutableString *buffer; + +- (instancetype)initWithTransformer:(id)transformer; + +@end diff --git a/MarkDownEditor/Classes/CMHTMLElement.m b/MarkDownEditor/Classes/CMHTMLElement.m new file mode 100755 index 0000000..5f3ae60 --- /dev/null +++ b/MarkDownEditor/Classes/CMHTMLElement.m @@ -0,0 +1,28 @@ +// +// CMHTMLElement.m +// MarkDownEditor +// +// Created by Indragie on 1/16/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMHTMLElement.h" +#import "CMHTMLElementTransformer.h" + +@implementation CMHTMLElement + +- (instancetype)initWithTransformer:(id)transformer +{ + if ((self = [super init])) { + _transformer = transformer; + _buffer = [[NSMutableString alloc] init]; + } + return self; +} + +- (NSString *)tagName; +{ + return [_transformer.class tagName]; +} + +@end diff --git a/MarkDownEditor/Classes/CMHTMLElementTransformer.h b/MarkDownEditor/Classes/CMHTMLElementTransformer.h new file mode 100755 index 0000000..b92b170 --- /dev/null +++ b/MarkDownEditor/Classes/CMHTMLElementTransformer.h @@ -0,0 +1,39 @@ +// +// CMHTMLElementTransformer.h +// MarkDownEditor +// +// Created by Indragie on 1/16/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import + +@class ONOXMLElement; + +/** + * Interface for an object that can transform an HTML element to an attributed string. + */ +@protocol CMHTMLElementTransformer +/** + * @return The name of the tag that this transformer handles. + */ ++ (NSString *)tagName; + +/** + * Transforms an HTML element to an attributed string. + * + * @param element The HTML element to transform. + * @param attributes The base attributes to be applied to the attributed string. + * + * @return An attributed string. + */ +- (NSAttributedString *)attributedStringForElement:(ONOXMLElement *)element attributes:(NSDictionary *)attributes; + +@end + +/** + * Use this macro inside an implementation of `-attributedStringForElement:attributes:` + * to assert that the root element's tag matches the transformer's tag. + */ +#define CMAssertCorrectTag(element) \ + NSAssert([element.tag isEqualToString:self.class.tagName], @"Tag does not match"); diff --git a/MarkDownEditor/Classes/CMHTMLRenderer.h b/MarkDownEditor/Classes/CMHTMLRenderer.h new file mode 100755 index 0000000..8e8f350 --- /dev/null +++ b/MarkDownEditor/Classes/CMHTMLRenderer.h @@ -0,0 +1,35 @@ +// +// CMHTMLRenderer.h +// MarkDownEditor +// +// Created by Indragie on 1/20/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import + +@class CMDocument; + +/** + * Renders HTML from a Markdown document. + */ +@interface CMHTMLRenderer : NSObject + +/** + * Designated initializer. + * + * @param document A Markdown document. + * + * @return An initialized instance of the receiver. + */ +- (instancetype)initWithDocument:(CMDocument *)document; + +/** + * Renders HTML from the Markdown document. + * + * @return A string containing the HTML representation + * of the document. + */ +- (NSString *)render; + +@end diff --git a/MarkDownEditor/Classes/CMHTMLRenderer.m b/MarkDownEditor/Classes/CMHTMLRenderer.m new file mode 100755 index 0000000..0c75146 --- /dev/null +++ b/MarkDownEditor/Classes/CMHTMLRenderer.m @@ -0,0 +1,31 @@ +// +// CMHTMLRenderer.m +// MarkDownEditor +// +// Created by Indragie on 1/20/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMHTMLRenderer.h" +#import "CMDocument_Private.h" +#import "CMNode_Private.h" + +@implementation CMHTMLRenderer { + CMDocument *_document; +} + +- (instancetype)initWithDocument:(CMDocument *)document +{ + if ((self = [super init])) { + _document = document; + } + return self; +} + +- (NSString *)render +{ + char *html = cmark_render_html(_document.rootNode.node, _document.options); + return [NSString stringWithUTF8String:html]; +} + +@end diff --git a/MarkDownEditor/Classes/CMHTMLScriptTransformer.h b/MarkDownEditor/Classes/CMHTMLScriptTransformer.h new file mode 100755 index 0000000..54ae13c --- /dev/null +++ b/MarkDownEditor/Classes/CMHTMLScriptTransformer.h @@ -0,0 +1,13 @@ +// +// CMHTMLScriptTransformer.h +// MarkDownEditor +// +// Created by Indragie on 1/16/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import +#import "CMHTMLElementTransformer.h" + +@interface CMHTMLScriptTransformer : NSObject +@end diff --git a/MarkDownEditor/Classes/CMHTMLScriptTransformer.m b/MarkDownEditor/Classes/CMHTMLScriptTransformer.m new file mode 100755 index 0000000..9e438d8 --- /dev/null +++ b/MarkDownEditor/Classes/CMHTMLScriptTransformer.m @@ -0,0 +1,67 @@ +// +// CMHTMLScriptTransformer.m +// MarkDownEditor +// +// Created by Indragie on 1/16/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMHTMLScriptTransformer.h" +#import "CMHTMLScriptTransformer_Private.h" +#import "CMPlatformDefines.h" + +#import "Ono.h" + +#if TARGET_OS_IPHONE +#import +#endif + +@implementation CMHTMLScriptTransformer { + CMHTMLScriptStyle _style; + CGFloat _fontSizeRatio; + CGFloat _baselineOffset; +} + +- (instancetype)initWithStyle:(CMHTMLScriptStyle)style fontSizeRatio:(CGFloat)ratio baselineOffset:(CGFloat)offset +{ + if ((self = [super init])) { + _style = style; + _fontSizeRatio = ratio; + _baselineOffset = offset; + } + return self; +} + +#pragma mark - CMHTMLElementTransformer + ++ (NSString *)tagName +{ + NSAssert(NO, @"Must be implemented by a subclass"); + return nil; +} + +- (NSAttributedString *)attributedStringForElement:(ONOXMLElement *)element attributes:(NSDictionary *)attributes +{ + CMAssertCorrectTag(element); + + NSMutableDictionary *allAttributes = [attributes mutableCopy]; + NSString *superscriptAttribute = nil; +#if TARGET_OS_IPHONE + superscriptAttribute = (__bridge NSString *)kCTSuperscriptAttributeName; +#else + superscriptAttribute = NSSuperscriptAttributeName; +#endif + allAttributes[superscriptAttribute] = @(_style); + CMFont *font = attributes[NSFontAttributeName]; + if (font != nil) { + font = [CMFont fontWithDescriptor:font.fontDescriptor size:font.pointSize * _fontSizeRatio]; + allAttributes[NSFontAttributeName] = font; + } + if (_baselineOffset != 0.0) { + allAttributes[NSBaselineOffsetAttributeName] = @(_baselineOffset); + } + + return [[NSAttributedString alloc] initWithString:element.stringValue attributes:allAttributes]; +} + +@end diff --git a/MarkDownEditor/Classes/CMHTMLScriptTransformer_Private.h b/MarkDownEditor/Classes/CMHTMLScriptTransformer_Private.h new file mode 100755 index 0000000..2cf0361 --- /dev/null +++ b/MarkDownEditor/Classes/CMHTMLScriptTransformer_Private.h @@ -0,0 +1,24 @@ +// +// CMHTMLScriptTransformer_Private.h +// MarkDownEditor +// +// Created by Indragie on 1/16/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMHTMLScriptTransformer.h" + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +typedef NS_ENUM(NSInteger, CMHTMLScriptStyle) { + CMHTMLScriptStyleSubscript = -1, + CMHTMLScriptStyleSuperscript = 1 +}; + +@interface CMHTMLScriptTransformer () +- (instancetype)initWithStyle:(CMHTMLScriptStyle)style fontSizeRatio:(CGFloat)ratio baselineOffset:(CGFloat)offset; +@end diff --git a/MarkDownEditor/Classes/CMHTMLStrikethroughTransformer.h b/MarkDownEditor/Classes/CMHTMLStrikethroughTransformer.h new file mode 100755 index 0000000..70d09fa --- /dev/null +++ b/MarkDownEditor/Classes/CMHTMLStrikethroughTransformer.h @@ -0,0 +1,37 @@ +// +// CMHTMLStrikethroughTransformer.h +// MarkDownEditor +// +// Created by Indragie on 1/16/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import +#import "CMHTMLElementTransformer.h" +#import "CMPlatformDefines.h" + +/** + Transforms HTML strikethrough elements () into attributed strings. + */ +@interface CMHTMLStrikethroughTransformer : NSObject + +/** + * Initializes the receiver with the default attributes -- a single line + * style and a color that matches the color of the text. + * + * @return An initialized instance of the receiver. + */ +- (instancetype)init; + +/** + * Initializes the receiver with a custom style and color. + * + * @param style Strikethrough style. + * @param color Strikethrough color. If `nil`, the transformer uses + * the color of the text if it has been specified. + * + * @return An initialized instance of the receiver. + */ +- (instancetype)initWithStrikethroughStyle:(CMUnderlineStyle)style color:(CMColor *)color; + +@end diff --git a/MarkDownEditor/Classes/CMHTMLStrikethroughTransformer.m b/MarkDownEditor/Classes/CMHTMLStrikethroughTransformer.m new file mode 100755 index 0000000..0578e3c --- /dev/null +++ b/MarkDownEditor/Classes/CMHTMLStrikethroughTransformer.m @@ -0,0 +1,50 @@ +// +// CMHTMLStrikethroughTransformer.m +// MarkDownEditor +// +// Created by Indragie on 1/16/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMHTMLStrikethroughTransformer.h" +#import "Ono.h" + +@implementation CMHTMLStrikethroughTransformer { + CMUnderlineStyle _style; + CMColor *_color; +} + +- (instancetype)init +{ + return [self initWithStrikethroughStyle:NSUnderlineStyleSingle color:nil]; +} + +- (instancetype)initWithStrikethroughStyle:(CMUnderlineStyle)style color:(CMColor *)color +{ + if ((self = [super init])) { + _style = style; + _color = color; + } + return self; +} + +#pragma mark - CMHTMLElementTransformer + ++ (NSString *)tagName { return @"s"; }; + +- (NSAttributedString *)attributedStringForElement:(ONOXMLElement *)element attributes:(NSDictionary *)attributes +{ + CMAssertCorrectTag(element); + + NSMutableDictionary *allAttributes = [attributes mutableCopy]; + allAttributes[NSStrikethroughStyleAttributeName] = @(_style); + + CMColor *color = _color ?: allAttributes[NSForegroundColorAttributeName]; + if (color != nil) { + allAttributes[NSStrikethroughColorAttributeName] = color; + } + + return [[NSAttributedString alloc] initWithString:element.stringValue attributes:allAttributes]; +} + +@end diff --git a/MarkDownEditor/Classes/CMHTMLSubscriptTransformer.h b/MarkDownEditor/Classes/CMHTMLSubscriptTransformer.h new file mode 100755 index 0000000..78ac443 --- /dev/null +++ b/MarkDownEditor/Classes/CMHTMLSubscriptTransformer.h @@ -0,0 +1,51 @@ +// +// CMHTMLSubscriptTransformer.h +// MarkDownEditor +// +// Created by Indragie on 1/16/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import +#import "CMHTMLScriptTransformer.h" + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +/** + * Transforms HTML subscript elements () into attributed strings. + */ +@interface CMHTMLSubscriptTransformer : CMHTMLScriptTransformer + +/** + * Initializes the receiver with the default font ratio (0.7) + * + * @return An initialized instance of the receiver. + */ +- (instancetype)init; + +/** + * Initializes the receiver with a custom font size ratio. + * + * @param ratio The factor to multiply the existing font point + * size by to calculate the size of the subscript font. + * + * @return An initialized instance of the receiver. + */ +- (instancetype)initWithFontSizeRatio:(CGFloat)ratio; + +/** + * Initializes the receiver with a custom font size ratio and a custom baseline offset. + * + * @param ratio The factor to multiply the existing font point + * size by to calculate the size of the superscript font. + * @param offset The offset for the baseline of the subscript. + * + * @return An initialized instance of the receiver. + */ +- (instancetype)initWithFontSizeRatio:(CGFloat)ratio baselineOffset:(CGFloat)offset; + +@end diff --git a/MarkDownEditor/Classes/CMHTMLSubscriptTransformer.m b/MarkDownEditor/Classes/CMHTMLSubscriptTransformer.m new file mode 100755 index 0000000..a1ed98c --- /dev/null +++ b/MarkDownEditor/Classes/CMHTMLSubscriptTransformer.m @@ -0,0 +1,33 @@ +// +// CMHTMLSubscriptTransformer.m +// MarkDownEditor +// +// Created by Indragie on 1/16/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMHTMLSubscriptTransformer.h" +#import "CMHTMLScriptTransformer_Private.h" + +@implementation CMHTMLSubscriptTransformer + +- (instancetype)init +{ + return [self initWithFontSizeRatio:0.7]; +} + +- (instancetype)initWithFontSizeRatio:(CGFloat)ratio +{ + return [self initWithStyle:CMHTMLScriptStyleSubscript fontSizeRatio:ratio baselineOffset:0.0]; +} + +- (instancetype)initWithFontSizeRatio:(CGFloat)ratio baselineOffset:(CGFloat)offset +{ + return [super initWithStyle:CMHTMLScriptStyleSubscript fontSizeRatio:ratio baselineOffset:offset]; +} + +#pragma mark - CMHTMLElementTransformer + ++ (NSString *)tagName { return @"sub"; }; + +@end diff --git a/MarkDownEditor/Classes/CMHTMLSuperscriptTransformer.h b/MarkDownEditor/Classes/CMHTMLSuperscriptTransformer.h new file mode 100755 index 0000000..85998b8 --- /dev/null +++ b/MarkDownEditor/Classes/CMHTMLSuperscriptTransformer.h @@ -0,0 +1,51 @@ +// +// CMHTMLSuperscriptTransformer.h +// MarkDownEditor +// +// Created by Indragie on 1/16/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import +#import "CMHTMLScriptTransformer.h" + +#if TARGET_OS_IPHONE +#import +#else +#import +#endif + +/** + * Transforms HTML superscript elements () into attributed strings. + */ +@interface CMHTMLSuperscriptTransformer : CMHTMLScriptTransformer + +/** + * Initializes the receiver with the default font ratio (0.7) + * + * @return An initialized instance of the receiver. + */ +- (instancetype)init; + +/** + * Initializes the receiver with a custom font size ratio and a default baseline offset. + * + * @param ratio The factor to multiply the existing font point + * size by to calculate the size of the superscript font. + * + * @return An initialized instance of the receiver. + */ +- (instancetype)initWithFontSizeRatio:(CGFloat)ratio; + +/** + * Initializes the receiver with a custom font size ratio and a custom baseline offset. + * + * @param ratio The factor to multiply the existing font point + * size by to calculate the size of the superscript font. + * @param offset The offset for the baseline of the superscript. + * + * @return An initialized instance of the receiver. + */ +- (instancetype)initWithFontSizeRatio:(CGFloat)ratio baselineOffset:(CGFloat)offset; + +@end diff --git a/MarkDownEditor/Classes/CMHTMLSuperscriptTransformer.m b/MarkDownEditor/Classes/CMHTMLSuperscriptTransformer.m new file mode 100755 index 0000000..7f7faa4 --- /dev/null +++ b/MarkDownEditor/Classes/CMHTMLSuperscriptTransformer.m @@ -0,0 +1,33 @@ +// +// CMHTMLSuperscriptTransformer.m +// MarkDownEditor +// +// Created by Indragie on 1/16/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMHTMLSuperscriptTransformer.h" +#import "CMHTMLScriptTransformer_Private.h" + +@implementation CMHTMLSuperscriptTransformer + +- (instancetype)init +{ + return [self initWithFontSizeRatio:0.7]; +} + +- (instancetype)initWithFontSizeRatio:(CGFloat)ratio +{ + return [self initWithStyle:CMHTMLScriptStyleSuperscript fontSizeRatio:ratio baselineOffset:0.0]; +} + +- (instancetype)initWithFontSizeRatio:(CGFloat)ratio baselineOffset:(CGFloat)offset +{ + return [super initWithStyle:CMHTMLScriptStyleSuperscript fontSizeRatio:ratio baselineOffset:offset]; +} + +#pragma mark - CMHTMLElementTransformer + ++ (NSString *)tagName { return @"sup"; }; + +@end diff --git a/MarkDownEditor/Classes/CMHTMLUnderlineTransformer.h b/MarkDownEditor/Classes/CMHTMLUnderlineTransformer.h new file mode 100755 index 0000000..9593134 --- /dev/null +++ b/MarkDownEditor/Classes/CMHTMLUnderlineTransformer.h @@ -0,0 +1,37 @@ +// +// CMHTMLUnderlineTransformer.h +// MarkDownEditor +// +// Created by Damien Rambout on 19/01/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import +#import "CMHTMLElementTransformer.h" +#import "CMPlatformDefines.h" + +/** + Transforms HTML underline elements () into attributed strings. + */ +@interface CMHTMLUnderlineTransformer : NSObject + +/** + * Initializes the receiver with the default attributes -- a single line + * style and a color that matches the color of the text. + * + * @return An initialized instance of the receiver. + */ +- (instancetype)init; + +/** + * Initializes the receiver with a custom style and color. + * + * @param style Strikethrough style. + * @param color Strikethrough color. If `nil`, the transformer uses + * the color of the text if it has been specified. + * + * @return An initialized instance of the receiver. + */ +- (instancetype)initWithUnderlineStyle:(CMUnderlineStyle)style color:(CMColor *)color; + +@end diff --git a/MarkDownEditor/Classes/CMHTMLUnderlineTransformer.m b/MarkDownEditor/Classes/CMHTMLUnderlineTransformer.m new file mode 100755 index 0000000..7102059 --- /dev/null +++ b/MarkDownEditor/Classes/CMHTMLUnderlineTransformer.m @@ -0,0 +1,50 @@ +// +// CMHTMLUnderlineTransformer.m +// MarkDownEditor +// +// Created by Damien Rambout on 21/01/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMHTMLUnderlineTransformer.h" +#import "Ono.h" + +@implementation CMHTMLUnderlineTransformer { + CMUnderlineStyle _style; + CMColor *_color; +} + +- (instancetype)init +{ + return [self initWithUnderlineStyle:NSUnderlineStyleSingle color:nil]; +} + +- (instancetype)initWithUnderlineStyle:(CMUnderlineStyle)style color:(CMColor *)color +{ + if ((self = [super init])) { + _style = style; + _color = color; + } + return self; +} + +#pragma mark - CMHTMLElementTransformer + ++ (NSString *)tagName { return @"u"; }; + +- (NSAttributedString *)attributedStringForElement:(ONOXMLElement *)element attributes:(NSDictionary *)attributes +{ + CMAssertCorrectTag(element); + + NSMutableDictionary *allAttributes = [attributes mutableCopy]; + allAttributes[NSUnderlineStyleAttributeName] = @(_style); + + CMColor *color = _color ?: allAttributes[NSForegroundColorAttributeName]; + if (color != nil) { + allAttributes[NSUnderlineColorAttributeName] = color; + } + + return [[NSAttributedString alloc] initWithString:element.stringValue attributes:allAttributes]; +} + +@end diff --git a/MarkDownEditor/Classes/CMHTMLUtilities.h b/MarkDownEditor/Classes/CMHTMLUtilities.h new file mode 100755 index 0000000..4647989 --- /dev/null +++ b/MarkDownEditor/Classes/CMHTMLUtilities.h @@ -0,0 +1,14 @@ +// +// CMHTMLUtilities.h +// MarkDownEditor +// +// Created by Indragie on 1/16/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import + +NSString * CMTagNameFromHTMLTag(NSString *tag); +BOOL CMIsHTMLVoidTagName(NSString *name); +BOOL CMIsHTMLTag(NSString *tag); +BOOL CMIsHTMLClosingTag(NSString *tag); diff --git a/MarkDownEditor/Classes/CMHTMLUtilities.m b/MarkDownEditor/Classes/CMHTMLUtilities.m new file mode 100755 index 0000000..7ec5e60 --- /dev/null +++ b/MarkDownEditor/Classes/CMHTMLUtilities.m @@ -0,0 +1,61 @@ +// +// CMHTMLUtilities.m +// MarkDownEditor +// +// Created by Indragie on 1/16/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMHTMLUtilities.h" + +NSString * CMTagNameFromHTMLTag(NSString *tag) +{ + // Assumes a well-formed HTML tag. + NSCharacterSet *alphanumeric = NSCharacterSet.alphanumericCharacterSet; + NSUInteger start = [tag rangeOfCharacterFromSet:alphanumeric].location; + if (start == NSNotFound) return nil; + + NSCharacterSet *inverseAlphanumeric = alphanumeric.invertedSet; + NSUInteger end = [tag rangeOfCharacterFromSet:inverseAlphanumeric options:0 range:NSMakeRange(start, tag.length - start)].location; + if (end == NSNotFound) return nil; + + return [tag substringWithRange:NSMakeRange(start, end - start)]; +} + +BOOL CMIsHTMLVoidTagName(NSString *name) +{ + static NSSet *voidNames = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + voidNames = [NSSet setWithObjects: + @"area", + @"base", + @"br", + @"col", + @"command", + @"embed", + @"hr", + @"img", + @"input", + @"keygen", + @"link", + @"meta", + @"param", + @"source", + @"track", + @"wbr", + nil + ]; + }); + return [voidNames containsObject:name]; +} + +BOOL CMIsHTMLTag(NSString *tag) +{ + return [tag hasPrefix:@"<"] && [tag hasSuffix:@">"]; +} + +BOOL CMIsHTMLClosingTag(NSString *tag) +{ + return [tag hasPrefix:@""]; +} diff --git a/MarkDownEditor/Classes/CMIterator.h b/MarkDownEditor/Classes/CMIterator.h new file mode 100755 index 0000000..73345fb --- /dev/null +++ b/MarkDownEditor/Classes/CMIterator.h @@ -0,0 +1,63 @@ +// +// CMIterator.h +// MarkDownEditor +// +// Created by Indragie on 1/13/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import + +typedef NS_ENUM(NSInteger, CMEventType) { + CMEventTypeNone, + CMEventTypeDone, + CMEventTypeEnter, + CMEventTypeExit +}; + +@class CMNode; + +/** + * Walks through a tree of nodes. + */ +@interface CMIterator : NSObject + +/** + * Initializes the receiver with a root node. + * + * @param rootNode Root node to start traversing from. + * + * @return An initialized instance of the receiver. + */ +- (instancetype)initWithRootNode:(CMNode *)rootNode; + +/** + * Returns the current node. + */ +@property (readonly) CMNode *currentNode; + +/** + * Returns the current event type. + */ +@property (readonly) CMEventType currentEvent; + +/** + * Walks through a tree of nodes, starting from the root node. + * + * @discussion See the section on iterators in cmark.h for more details. + * + * @param block Block to call upon entering or exiting a node during traversal. + * Set `stop` to `YES` to stop iteration. + */ +- (void)enumerateUsingBlock:(void (^)(CMNode *node, CMEventType event, BOOL *stop))block; + +/** + * Resets the iterator to the specified node and event. The node must be either + * the root node or a child of the root node. + * + * @param node The node to reset to. + * @param eventType The event to reset to. + */ +- (void)resetToNode:(CMNode *)node withEventType:(CMEventType)eventType; + +@end diff --git a/MarkDownEditor/Classes/CMIterator.m b/MarkDownEditor/Classes/CMIterator.m new file mode 100755 index 0000000..68c5521 --- /dev/null +++ b/MarkDownEditor/Classes/CMIterator.m @@ -0,0 +1,67 @@ +// +// CMIterator.m +// MarkDownEditor +// +// Created by Indragie on 1/13/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMIterator.h" +#import "CMNode_Private.h" + +@implementation CMIterator { + cmark_iter *_iter; +} + +#pragma mark - Initialization + +- (instancetype)initWithRootNode:(CMNode *)rootNode +{ + NSParameterAssert(rootNode); + + if ((self = [super init])) { + _iter = cmark_iter_new(rootNode.node); + if (_iter == NULL) return nil; + } + return self; +} + +- (void)dealloc +{ + cmark_iter_free(_iter); +} + +#pragma mark - Accessors + +- (CMNode *)currentNode +{ + return [[CMNode alloc] initWithNode:cmark_iter_get_node(_iter) freeWhenDone:NO]; +} + +- (CMEventType)currentEvent +{ + return (CMEventType)cmark_iter_get_event_type(_iter); +} + +#pragma mark - Iteration + +- (void)enumerateUsingBlock:(void (^)(CMNode *node, CMEventType event, BOOL *stop))block +{ + NSParameterAssert(block); + + cmark_event_type event; + BOOL stop = NO; + + while ((event = cmark_iter_next(_iter)) != CMARK_EVENT_DONE) { + CMNode *currentNode = [[CMNode alloc] initWithNode:cmark_iter_get_node(_iter) freeWhenDone:NO]; + block(currentNode, (CMEventType)event, &stop); + if (stop) break; + } +} + +- (void)resetToNode:(CMNode *)node withEventType:(CMEventType)eventType +{ + cmark_iter_reset(_iter, node.node, (cmark_event_type)eventType); +} + +@end diff --git a/MarkDownEditor/Classes/CMNode.h b/MarkDownEditor/Classes/CMNode.h new file mode 100755 index 0000000..2efa3bd --- /dev/null +++ b/MarkDownEditor/Classes/CMNode.h @@ -0,0 +1,174 @@ +// +// CMNode.h +// MarkDownEditor +// +// Created by Indragie on 1/12/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import + +@class CMIterator; + +typedef NS_ENUM(NSInteger, CMNodeType) { + /* Error status */ + CMNodeTypeNone, + + /* Block */ + CMNodeTypeDocument, + CMNodeTypeBlockQuote, + CMNodeTypeList, + CMNodeTypeItem, + CMNodeTypeCodeBlock, + CMNodeTypeHTML, + CMNodeTypeParagraph, + CMNodeTypeHeader, + CMNodeTypeHRule, + + CMNodeTypeFirstBlock = CMNodeTypeDocument, + CMNodeTypeLastBlock = CMNodeTypeHRule, + + /* Inline */ + CMNodeTypeText, + CMNodeTypeSoftbreak, + CMNodeTypeLinebreak, + CMNodeTypeCode, + CMNodeTypeInlineHTML, + CMNodeTypeEmphasis, + CMNodeTypeStrong, + CMNodeTypeLink, + CMNodeTypeImage, + + CMNodeTypeFirstInline = CMNodeTypeText, + CMNodeTypeLastInline = CMNodeTypeImage +}; + +typedef NS_ENUM(NSInteger, CMListType) { + CMListTypeNone, + CMListTypeUnordered, + CMListTypeOrdered +}; + +typedef NS_ENUM(NSInteger, CMDelimeterType) { + CMDelimeterTypeNone, + CMDelimeterTypePeriod, + CMDelimeterTypeParen +}; + +/** + * Immutable interface to a CommonMark node. + */ +@interface CMNode : NSObject + +/** + * Creates an iterator for the node tree that has the + * receiver as its root. + * + * @return A new iterator. + */ +- (CMIterator *)iterator; + +/** + * The next node in the sequence, or `nil` if there is none. + */ +@property (readonly) CMNode *next; + +/** + * The previous node in the sequence, or `nil` if there is none. + */ +@property (readonly) CMNode *previous; + +/** + * The receiver's parent node, or `nil` if there is none. + */ +@property (readonly) CMNode *parent; + +/** + * The first child node of the receiver, or `nil` if there is none. + */ +@property (readonly) CMNode *firstChild; + +/** + * The last child node of the receiver, or `nil` if there is none. + */ +@property (readonly) CMNode *lastChild; + +/** + * The type of the node, or `CMNodeTypeNone` on error. + */ +@property (readonly) CMNodeType type; + +/** + * String representation of `type`. + */ +@property (readonly) NSString *humanReadableType; + +/** + * String contents of the receiver, or `nil` if there is none. + */ +@property (readonly) NSString *stringValue; + +/** + * Header level of the receiver, or `0` if the receiver is not a header. + */ +@property (readonly) NSInteger headerLevel; + +/** + * Info string from a fenced code block, or `nil` if there is none. + */ +@property (readonly) NSString *fencedCodeInfo; + +/** + * The receiver's list type, or `CMListTypeNone` if the receiver + * is not a list. + */ +@property (readonly) CMListType listType; + +/** + * The receiver's list delimeter type, or `CMDelimeterTypeNone` if the + * receiver is not a list. + */ +@property (readonly) CMDelimeterType listDelimeterType; + +/** + * Starting number of the list, or `0` if the receiver is not + * an ordered list. + */ +@property (readonly) NSInteger listStartingNumber; + +/** + * `YES` if the receiver is a tight list, `NO` otherwise. + */ +@property (readonly) BOOL listTight; + +/** + * Link or image URL, or `nil` if there is none. + */ +@property (readonly) NSURL *URL; + +/** + * Link or image title, or `nil` if there is none. + */ +@property (readonly) NSString *title; + +/** + * The line on which the receiver begins. + */ +@property (readonly) NSInteger startLine; + +/** + * The column on which the receiver begins. + */ +@property (readonly) NSInteger startColumn; + +/** + * The line on which the receiver ends. + */ +@property (readonly) NSInteger endLine; + +/** + * The column on which the receiver ends. + */ +@property (readonly) NSInteger endColumn; + +@end diff --git a/MarkDownEditor/Classes/CMNode.m b/MarkDownEditor/Classes/CMNode.m new file mode 100755 index 0000000..6af0db6 --- /dev/null +++ b/MarkDownEditor/Classes/CMNode.m @@ -0,0 +1,234 @@ +// +// CMNode.m +// MarkDownEditor +// +// Created by Indragie on 1/12/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMNode.h" +#import "CMNode_Private.h" +#import "CMIterator.h" + +static CMNode * wrap(cmark_node *node) { + if (node == NULL) return nil; + + return [[CMNode alloc] initWithNode:node freeWhenDone:NO]; +} + +static NSString * str(const char *buf) { + if (buf == NULL) return nil; + + return [NSString stringWithUTF8String:buf]; +} + +static NSString * NSStringFromCMNodeType(CMNodeType type) { + switch (type) { + case CMNodeTypeBlockQuote: + return @"Block Quote"; + case CMNodeTypeCode: + return @"Code"; + case CMNodeTypeCodeBlock: + return @"Code Block"; + case CMNodeTypeDocument: + return @"Document"; + case CMNodeTypeEmphasis: + return @"Emphasis"; + case CMNodeTypeHeader: + return @"Header"; + case CMNodeTypeHRule: + return @"Horizontal Rule"; + case CMNodeTypeHTML: + return @"HTML"; + case CMNodeTypeImage: + return @"Image"; + case CMNodeTypeInlineHTML: + return @"Inline HTML"; + case CMNodeTypeItem: + return @"List Item"; + case CMNodeTypeLinebreak: + return @"Linebreak"; + case CMNodeTypeLink: + return @"Link"; + case CMNodeTypeList: + return @"List"; + case CMNodeTypeNone: + return @"None"; + case CMNodeTypeParagraph: + return @"Paragraph"; + case CMNodeTypeSoftbreak: + return @"Softbreak"; + case CMNodeTypeStrong: + return @"Strong"; + case CMNodeTypeText: + return @"Text"; + default: return nil; + } +} + +@implementation CMNode { + BOOL _freeWhenDone; +} + +#pragma mark - Initialization + +- (instancetype)init +{ + NSAssert(NO, @"CMNode instance can not be created."); + return nil; +} + +- (instancetype)initWithNode:(cmark_node *)node freeWhenDone:(BOOL)free +{ + NSParameterAssert(node); + + if ((self = [super init])) { + _node = node; + _freeWhenDone = free; + } + return self; +} + +- (void)dealloc +{ + if (_freeWhenDone) { + cmark_node_free(_node); + } +} + +#pragma mark - NSObject + +- (BOOL)isEqual:(CMNode *)node +{ + if (self == node) return YES; + if (![node isMemberOfClass:self.class]) return NO; + + return _node == node->_node; +} + +- (NSUInteger)hash +{ + return (NSUInteger)_node; +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<%@:%p type:%@ stringValue:%@>", self.class, self, NSStringFromCMNodeType(self.type), self.stringValue]; +} + +#pragma mark - Iteration + +- (CMIterator *)iterator +{ + return [[CMIterator alloc] initWithRootNode:self]; +} + +#pragma mark - Tree Traversal + +- (CMNode *)next +{ + return wrap(cmark_node_next(_node)); +} + +- (CMNode *)previous +{ + return wrap(cmark_node_previous(_node)); +} + +- (CMNode *)parent +{ + return wrap(cmark_node_parent(_node)); +} + +- (CMNode *)firstChild +{ + return wrap(cmark_node_first_child(_node)); +} + +- (CMNode *)lastChild +{ + return wrap(cmark_node_last_child(_node)); +} + +#pragma mark - Attributes + +- (CMNodeType)type +{ + return (CMNodeType)cmark_node_get_type(_node); +} + +- (NSString *)humanReadableType +{ + return str(cmark_node_get_type_string(_node)); +} + +- (NSString *)stringValue +{ + return str(cmark_node_get_literal(_node)); +} + +- (NSInteger)headerLevel +{ + return cmark_node_get_header_level(_node); +} + +- (NSString *)fencedCodeInfo +{ + return str(cmark_node_get_fence_info(_node)); +} + +- (CMListType)listType +{ + return (CMListType)cmark_node_get_list_type(_node); +} + +- (CMDelimeterType)listDelimeterType +{ + return (CMDelimeterType)cmark_node_get_list_delim(_node); +} + +- (NSInteger)listStartingNumber +{ + return cmark_node_get_list_start(_node); +} + +- (BOOL)listTight +{ + return (cmark_node_get_list_tight(_node) == 0) ? NO : YES; +} + +- (NSURL *)URL +{ + NSString *URLString = str(cmark_node_get_url(_node)); + if (URLString != nil) { + return [NSURL URLWithString:URLString]; + } + return nil; +} + +- (NSString *)title +{ + return str(cmark_node_get_title(_node)); +} + +- (NSInteger)startLine +{ + return cmark_node_get_start_line(_node); +} + +- (NSInteger)startColumn +{ + return cmark_node_get_start_column(_node); +} + +- (NSInteger)endLine +{ + return cmark_node_get_end_line(_node); +} + +- (NSInteger)endColumn +{ + return cmark_node_get_end_column(_node); +} + +@end diff --git a/MarkDownEditor/Classes/CMNode_Private.h b/MarkDownEditor/Classes/CMNode_Private.h new file mode 100755 index 0000000..7e87ccc --- /dev/null +++ b/MarkDownEditor/Classes/CMNode_Private.h @@ -0,0 +1,25 @@ +// +// CMNode_Private.h +// MarkDownEditor +// +// Created by Indragie on 1/13/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMNode.h" +#import + +@interface CMNode () +/** + * Designated initializer. + * + * @param node Pointer to the node to wrap. + * @param free Whether to free the underlying node when the + * receiver is deallocated. + * + * @return An initialized instance of the receiver. + */ +- (instancetype)initWithNode:(cmark_node *)node freeWhenDone:(BOOL)free; + +@property (readonly) cmark_node *node; +@end diff --git a/MarkDownEditor/Classes/CMParser.h b/MarkDownEditor/Classes/CMParser.h new file mode 100755 index 0000000..638332b --- /dev/null +++ b/MarkDownEditor/Classes/CMParser.h @@ -0,0 +1,116 @@ +// +// CMParser.h +// MarkDownEditor +// +// Created by Indragie on 1/13/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import + +@class CMNode; + +@class CMDocument; +@protocol CMParserDelegate; + +/** + * Not really a parser, but you can pretend it is. + * + * This class takes a `CMDocument` (which contains the tree for the already-parsed + * Markdown data) and traverses the tree to implement `NSXMLParser`-style delegate + * callbacks. + * + * This is useful for implementing custom renderers. + * + * @warning This class is not thread-safe and can only be accessed from a single + * thread at a time. + */ +@interface CMParser : NSObject + +/** + * Initializes the receiver with a document. + * + * @param document CommonMark document. + * @param delegate Delegate to receive callbacks during parsing. + * + * @return An initialized instance of the receiver. + */ +- (instancetype)initWithDocument:(CMDocument *)document delegate:(id)delegate; + +/** + * Document being parsed. + */ +@property (nonatomic, readonly) CMDocument *document; + +/** + * Delegate to receive callbacks during parsing. + */ +@property (nonatomic, weak, readonly) id delegate; + +/** + * Returns the node currently being parsed, or `nil` if not parsing. + */ +@property (atomic, readonly) CMNode *currentNode; + +/** + * Start parsing. + */ +- (void)parse; + +/** + * Stop parsing. If implemented, `-parserDidAbort:` will be called on the delegate. + */ +- (void)abortParsing; + +@end + +@protocol CMParserDelegate +@optional +- (void)parserDidStartDocument:(CMParser *)parser; +- (void)parserDidEndDocument:(CMParser *)parser; +- (void)parserDidAbort:(CMParser *)parser; + +- (void)parser:(CMParser *)parser foundText:(NSString *)text; +- (void)parserFoundHRule:(CMParser *)parser; + +- (void)parser:(CMParser *)parser didStartHeaderWithLevel:(NSInteger)level; +- (void)parser:(CMParser *)parser didEndHeaderWithLevel:(NSInteger)level; + +- (void)parserDidStartParagraph:(CMParser *)parser; +- (void)parserDidEndParagraph:(CMParser *)parser; + +- (void)parserDidStartEmphasis:(CMParser *)parser; +- (void)parserDidEndEmphasis:(CMParser *)parser; + +- (void)parserDidStartStrong:(CMParser *)parser; +- (void)parserDidEndStrong:(CMParser *)parser; + +- (void)parser:(CMParser *)parser didStartLinkWithURL:(NSURL *)URL title:(NSString *)title; +- (void)parser:(CMParser *)parser didEndLinkWithURL:(NSURL *)URL title:(NSString *)title; + +- (void)parser:(CMParser *)parser didStartImageWithURL:(NSURL *)URL title:(NSString *)title; +- (void)parser:(CMParser *)parser didEndImageWithURL:(NSURL *)URL title:(NSString *)title; + +- (void)parser:(CMParser *)parser foundHTML:(NSString *)HTML; +- (void)parser:(CMParser *)parser foundInlineHTML:(NSString *)HTML; + +- (void)parser:(CMParser *)parser foundCodeBlock:(NSString *)code info:(NSString *)info; +- (void)parser:(CMParser *)parser foundInlineCode:(NSString *)code; + +- (void)parserFoundSoftBreak:(CMParser *)parser; +- (void)parserFoundLineBreak:(CMParser *)parser; + +- (void)parserDidStartBlockQuote:(CMParser *)parser; +- (void)parserDidEndBlockQuote:(CMParser *)parser; + +- (void)parser:(CMParser *)parser didStartUnorderedListWithTightness:(BOOL)tight; +- (void)parser:(CMParser *)parser didEndUnorderedListWithTightness:(BOOL)tight; + +- (void)parser:(CMParser *)parser didStartOrderedListWithStartingNumber:(NSInteger)num tight:(BOOL)tight; +- (void)parser:(CMParser *)parser didEndOrderedListWithStartingNumber:(NSInteger)num tight:(BOOL)tight; + +- (void)parserDidStartListItem:(CMParser *)parser; +- (void)parserDidEndListItem:(CMParser *)parser; + +@end + diff --git a/MarkDownEditor/Classes/CMParser.m b/MarkDownEditor/Classes/CMParser.m new file mode 100755 index 0000000..3c8c1a2 --- /dev/null +++ b/MarkDownEditor/Classes/CMParser.m @@ -0,0 +1,290 @@ +// +// CMParser.m +// MarkDownEditor +// +// Created by Indragie on 1/13/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMParser.h" +#import "CMDocument.h" +#import "CMIterator.h" +#import "CMNode.h" + +#import + +@interface CMParser () +@property (atomic, readwrite) CMNode *currentNode; +@property (nonatomic, weak, readwrite) id delegate; +@end + +@implementation CMParser { + struct { + unsigned int didStartDocument:1; + unsigned int didEndDocument:1; + unsigned int didAbort:1; + unsigned int foundText:1; + unsigned int foundHRule:1; + unsigned int didStartHeader:1; + unsigned int didEndHeader:1; + unsigned int didStartParagraph:1; + unsigned int didEndParagraph:1; + unsigned int didStartEmphasis:1; + unsigned int didEndEmphasis:1; + unsigned int didStartStrong:1; + unsigned int didEndStrong:1; + unsigned int didStartLink:1; + unsigned int didEndLink:1; + unsigned int didStartImage:1; + unsigned int didEndImage:1; + unsigned int foundHTML:1; + unsigned int foundInlineHTML:1; + unsigned int foundCodeBlock:1; + unsigned int foundInlineCode:1; + unsigned int foundSoftBreak:1; + unsigned int foundLineBreak:1; + unsigned int didStartBlockQuote:1; + unsigned int didEndBlockQuote:1; + unsigned int didStartUnorderedList:1; + unsigned int didEndUnorderedList:1; + unsigned int didStartOrderedList:1; + unsigned int didEndOrderedList:1; + unsigned int didStartListItem:1; + unsigned int didEndListItem:1; + } _delegateFlags; + volatile int32_t _parsing; +} + +#pragma mark - Initialization + +- (instancetype)initWithDocument:(CMDocument *)document delegate:(id)delegate +{ + NSParameterAssert(document); + NSParameterAssert(delegate); + + if ((self = [super init])) { + _document = document; + self.delegate = delegate; + } + return self; +} + +#pragma mark - Parsing + +- (void)parse +{ + if (!OSAtomicCompareAndSwap32Barrier(0, 1, &_parsing)) return; + + [[_document.rootNode iterator] enumerateUsingBlock:^(CMNode *node, CMEventType event, BOOL *stop) { + self.currentNode = node; + [self handleNode:node event:event]; + if (_parsing == 0) *stop = YES; + }]; + + _parsing = 0; +} + +- (void)abortParsing +{ + if (!OSAtomicCompareAndSwap32Barrier(1, 0, &_parsing)) return; + + if (_delegateFlags.didAbort) { + [_delegate parserDidAbort:self]; + } +} + +- (void)handleNode:(CMNode *)node event:(CMEventType)event { + NSAssert((event == CMEventTypeEnter) || (event == CMEventTypeExit), @"Event must be either an exit or enter event"); + + switch (node.type) { + case CMNodeTypeDocument: + if (event == CMEventTypeEnter) { + if (_delegateFlags.didStartDocument) { + [_delegate parserDidStartDocument:self]; + } + } else if (_delegateFlags.didEndDocument) { + [_delegate parserDidEndDocument:self]; + } + break; + case CMNodeTypeText: + if (_delegateFlags.foundText) { + [_delegate parser:self foundText:node.stringValue]; + } + break; + case CMNodeTypeHRule: + if (_delegateFlags.foundHRule) { + [_delegate parserFoundHRule:self]; + } + break; + case CMNodeTypeHeader: + if (event == CMEventTypeEnter) { + if (_delegateFlags.didStartHeader) { + [_delegate parser:self didStartHeaderWithLevel:node.headerLevel]; + } + } else if (_delegateFlags.didEndHeader) { + [_delegate parser:self didEndHeaderWithLevel:node.headerLevel]; + } + break; + case CMNodeTypeParagraph: + if (event == CMEventTypeEnter) { + if (_delegateFlags.didStartParagraph) { + [_delegate parserDidStartParagraph:self]; + } + } else if (_delegateFlags.didEndParagraph) { + [_delegate parserDidEndParagraph:self]; + } + break; + case CMNodeTypeEmphasis: + if (event == CMEventTypeEnter) { + if (_delegateFlags.didStartEmphasis) { + [_delegate parserDidStartEmphasis:self]; + } + } else if (_delegateFlags.didEndEmphasis) { + [_delegate parserDidEndEmphasis:self]; + } + break; + case CMNodeTypeStrong: + if (event == CMEventTypeEnter) { + if (_delegateFlags.didStartStrong) { + [_delegate parserDidStartStrong:self]; + } + } else if (_delegateFlags.didEndStrong) { + [_delegate parserDidEndStrong:self]; + } + break; + case CMNodeTypeLink: + if (event == CMEventTypeEnter) { + if (_delegateFlags.didStartLink) { + [_delegate parser:self didStartLinkWithURL:node.URL title:node.title]; + } + } else if (_delegateFlags.didEndLink) { + [_delegate parser:self didEndLinkWithURL:node.URL title:node.title]; + } + break; + case CMNodeTypeImage: + if (event == CMEventTypeEnter) { + if (_delegateFlags.didStartImage) { + [_delegate parser:self didStartImageWithURL:node.URL title:node.title]; + } + } else if (_delegateFlags.didEndImage) { + [_delegate parser:self didEndImageWithURL:node.URL title:node.title]; + } + break; + case CMNodeTypeHTML: + if (_delegateFlags.foundHTML) { + [_delegate parser:self foundHTML:node.stringValue]; + } + break; + case CMNodeTypeInlineHTML: + if (_delegateFlags.foundInlineHTML) { + [_delegate parser:self foundInlineHTML:node.stringValue]; + } + break; + case CMNodeTypeCodeBlock: + if (_delegateFlags.foundCodeBlock) { + [_delegate parser:self foundCodeBlock:node.stringValue info:node.fencedCodeInfo]; + } + break; + case CMNodeTypeCode: + if (_delegateFlags.foundInlineCode) { + [_delegate parser:self foundInlineCode:node.stringValue]; + } + break; + case CMNodeTypeSoftbreak: + if (_delegateFlags.foundSoftBreak) { + [_delegate parserFoundSoftBreak:self]; + } + break; + case CMNodeTypeLinebreak: + if (_delegateFlags.foundLineBreak) { + [_delegate parserFoundLineBreak:self]; + } + break; + case CMNodeTypeBlockQuote: + if (event == CMEventTypeEnter) { + if (_delegateFlags.didStartBlockQuote) { + [_delegate parserDidStartBlockQuote:self]; + } + } else if (_delegateFlags.didEndBlockQuote) { + [_delegate parserDidEndBlockQuote:self]; + } + break; + case CMNodeTypeList: + switch (node.listType) { + case CMListTypeOrdered: + if (event == CMEventTypeEnter) { + if (_delegateFlags.didStartOrderedList) { + [_delegate parser:self didStartOrderedListWithStartingNumber:node.listStartingNumber tight:node.listTight]; + } + } else if (_delegateFlags.didEndOrderedList) { + [_delegate parser:self didEndOrderedListWithStartingNumber:node.listStartingNumber tight:node.listTight]; + } + break; + case CMListTypeUnordered: + if (event == CMEventTypeEnter) { + if (_delegateFlags.didStartUnorderedList) { + [_delegate parser:self didStartUnorderedListWithTightness:node.listTight]; + } + } else if (_delegateFlags.didEndUnorderedList) { + [_delegate parser:self didEndUnorderedListWithTightness:node.listTight]; + } + break; + default: + break; + } + break; + case CMNodeTypeItem: + if (event == CMEventTypeEnter) { + if (_delegateFlags.didStartListItem) { + [_delegate parserDidStartListItem:self]; + } + } else if (_delegateFlags.didEndListItem) { + [_delegate parserDidEndListItem:self]; + } + break; + default: + break; + } +} + +#pragma mark - Accessors + +- (void)setDelegate:(id)delegate +{ + if (_delegate != delegate) { + _delegate = delegate; + _delegateFlags.didStartDocument = [_delegate respondsToSelector:@selector(parserDidStartDocument:)]; + _delegateFlags.didEndDocument = [_delegate respondsToSelector:@selector(parserDidEndDocument:)]; + _delegateFlags.didAbort = [_delegate respondsToSelector:@selector(parserDidAbort:)]; + _delegateFlags.foundText = [_delegate respondsToSelector:@selector(parser:foundText:)]; + _delegateFlags.foundHRule = [_delegate respondsToSelector:@selector(parserFoundHRule:)]; + _delegateFlags.didStartHeader = [_delegate respondsToSelector:@selector(parser:didStartHeaderWithLevel:)]; + _delegateFlags.didEndHeader = [_delegate respondsToSelector:@selector(parser:didEndHeaderWithLevel:)]; + _delegateFlags.didStartParagraph = [_delegate respondsToSelector:@selector(parserDidStartParagraph:)]; + _delegateFlags.didEndParagraph = [_delegate respondsToSelector:@selector(parserDidEndParagraph:)]; + _delegateFlags.didStartEmphasis = [_delegate respondsToSelector:@selector(parserDidStartEmphasis:)]; + _delegateFlags.didEndEmphasis = [_delegate respondsToSelector:@selector(parserDidEndEmphasis:)]; + _delegateFlags.didStartStrong = [_delegate respondsToSelector:@selector(parserDidStartStrong:)]; + _delegateFlags.didEndStrong = [_delegate respondsToSelector:@selector(parserDidEndStrong:)]; + _delegateFlags.didStartLink = [_delegate respondsToSelector:@selector(parser:didStartLinkWithURL:title:)]; + _delegateFlags.didEndLink = [_delegate respondsToSelector:@selector(parser:didEndLinkWithURL:title:)]; + _delegateFlags.didStartImage = [_delegate respondsToSelector:@selector(parser:didStartImageWithURL:title:)]; + _delegateFlags.didEndImage = [_delegate respondsToSelector:@selector(parser:didEndImageWithURL:title:)]; + _delegateFlags.foundHTML = [_delegate respondsToSelector:@selector(parser:foundHTML:)]; + _delegateFlags.foundInlineHTML = [_delegate respondsToSelector:@selector(parser:foundInlineHTML:)]; + _delegateFlags.foundCodeBlock = [_delegate respondsToSelector:@selector(parser:foundCodeBlock:info:)]; + _delegateFlags.foundInlineCode = [_delegate respondsToSelector:@selector(parser:foundInlineCode:)]; + _delegateFlags.foundSoftBreak = [_delegate respondsToSelector:@selector(parserFoundSoftBreak:)]; + _delegateFlags.foundLineBreak = [_delegate respondsToSelector:@selector(parserFoundLineBreak:)]; + _delegateFlags.didStartBlockQuote = [_delegate respondsToSelector:@selector(parserDidStartBlockQuote:)]; + _delegateFlags.didEndBlockQuote = [_delegate respondsToSelector:@selector(parserDidEndBlockQuote:)]; + _delegateFlags.didStartUnorderedList = [_delegate respondsToSelector:@selector(parser:didStartUnorderedListWithTightness:)]; + _delegateFlags.didEndUnorderedList = [_delegate respondsToSelector:@selector(parser:didEndUnorderedListWithTightness:)]; + _delegateFlags.didStartOrderedList = [_delegate respondsToSelector:@selector(parser:didStartOrderedListWithStartingNumber:tight:)]; + _delegateFlags.didEndOrderedList = [_delegate respondsToSelector:@selector(parser:didEndOrderedListWithStartingNumber:tight:)]; + _delegateFlags.didStartListItem = [_delegate respondsToSelector:@selector(parserDidStartListItem:)]; + _delegateFlags.didEndListItem = [_delegate respondsToSelector:@selector(parserDidEndListItem:)]; + } +} + +@end diff --git a/MarkDownEditor/Classes/CMPlatformDefines.h b/MarkDownEditor/Classes/CMPlatformDefines.h new file mode 100755 index 0000000..0e22dea --- /dev/null +++ b/MarkDownEditor/Classes/CMPlatformDefines.h @@ -0,0 +1,36 @@ +// +// CMPlatformDefines.h +// MarkDownEditor +// +// Created by Indragie on 1/15/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#ifndef CocoaMarkdown_CMPlatformDefines_h +#define CocoaMarkdown_CMPlatformDefines_h + +#if TARGET_OS_IPHONE +#import + +#define CMColor UIColor +#define CMFontSymbolicTraits UIFontDescriptorSymbolicTraits +#define CMFont UIFont +#define CMFontDescriptor UIFontDescriptor +#define CMFontTraitItalic UIFontDescriptorTraitItalic +#define CMFontTraitBold UIFontDescriptorTraitBold +#define CMUnderlineStyle NSUnderlineStyle + +#else +#import + +#define CMColor NSColor +#define CMFontSymbolicTraits NSFontSymbolicTraits +#define CMFont NSFont +#define CMFontDescriptor NSFontDescriptor +#define CMFontTraitItalic NSFontItalicTrait +#define CMFontTraitBold NSFontBoldTrait +#define CMUnderlineStyle NSInteger + +#endif + +#endif diff --git a/MarkDownEditor/Classes/CMStack.h b/MarkDownEditor/Classes/CMStack.h new file mode 100755 index 0000000..adfc98b --- /dev/null +++ b/MarkDownEditor/Classes/CMStack.h @@ -0,0 +1,20 @@ +// +// CMStack.h +// MarkDownEditor +// +// Created by Indragie on 1/16/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import + +/** + * Array backed stack. + */ +@interface CMStack : NSObject +@property (nonatomic, readonly) NSArray *objects; + +- (void)push:(id)object; +- (id)pop; +- (id)peek; +@end diff --git a/MarkDownEditor/Classes/CMStack.m b/MarkDownEditor/Classes/CMStack.m new file mode 100755 index 0000000..35bbe7c --- /dev/null +++ b/MarkDownEditor/Classes/CMStack.m @@ -0,0 +1,41 @@ + +// +// CMStack.m +// MarkDownEditor +// +// Created by Indragie on 1/16/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMStack.h" + +@implementation CMStack { + NSMutableArray *_objects; +} + +- (instancetype)init +{ + if ((self = [super init])) { + _objects = [[NSMutableArray alloc] init]; + } + return self; +} + +- (void)push:(id)object +{ + [_objects addObject:object]; +} + +- (id)pop +{ + id object = _objects.lastObject; + [_objects removeLastObject]; + return object; +} + +- (id)peek +{ + return _objects.lastObject; +} + +@end diff --git a/MarkDownEditor/Classes/CMTextAttributes.h b/MarkDownEditor/Classes/CMTextAttributes.h new file mode 100755 index 0000000..f223cea --- /dev/null +++ b/MarkDownEditor/Classes/CMTextAttributes.h @@ -0,0 +1,187 @@ +// +// CMTextAttributes.h +// MarkDownEditor +// +// Created by Indragie on 1/15/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import + +/** + * Container for sets of text attributes used to style + * attributed strings. + */ +@interface CMTextAttributes : NSObject + +/** + * Initializes the receiver with the default attributes. + * + * @return An initialized instance of the receiver. + */ +- (instancetype)init; + +/** + * @param level The header level. + * + * @return The attributes for the specified header level. + */ +- (NSDictionary *)attributesForHeaderLevel:(NSInteger)level; + +/** + * Attributes used to style text. + * + * On iOS, defaults to using the Dynamic Type font with style `UIFontTextStyleBody` + * On OS X, defaults to using the user font with size 12pt. + */ +@property (nonatomic) NSDictionary *textAttributes; + +/** + * Attributes used to style level 1 headers. + * + * On iOS, defaults to using the Dynamic Type font with style `UIFontTextStyleHeadline` + * On OS X, defaults to using the user font with size 24pt. + */ +@property (nonatomic) NSDictionary *h1Attributes; + +/** + * Attributes used to style level 2 headers. + * + * On iOS, defaults to using the Dynamic Type font with style `UIFontTextStyleHeadline` + * On OS X, defaults to using the user font with size 18pt. + */ +@property (nonatomic) NSDictionary *h2Attributes; + +/** + * Attributes used to style level 3 headers. + * + * On iOS, defaults to using the Dynamic Type font with style `UIFontTextStyleHeadline` + * On OS X, defaults to using the user font with size 14pt. + */ +@property (nonatomic) NSDictionary *h3Attributes; + +/** + * Attributes used to style level 4 headers. + * + * On iOS, defaults to using the Dynamic Type font with style `UIFontTextStyleSubheadline` + * On OS X, defaults to using the user font with size 12pt. + */ +@property (nonatomic) NSDictionary *h4Attributes; + +/** + * Attributes used to style level 5 headers. + * + * On iOS, defaults to using the Dynamic Type font with style `UIFontTextStyleSubheadline` + * On OS X, defaults to using the user font with size 10pt. + */ +@property (nonatomic) NSDictionary *h5Attributes; + +/** + * Attributes used to style level 6 headers. + * + * On iOS, defaults to using the Dynamic Type font with style `UIFontTextStyleSubheadline` + * On OS X, defaults to using the user font with size 8pt. + */ +@property (nonatomic) NSDictionary *h6Attributes; + +/** + * Attributes used to style emphasized text. + * + * If not set, the renderer will attempt to infer the emphasized font from the + * regular text font. + */ +@property (nonatomic) NSDictionary *emphasisAttributes; + +/** + * Attributes used to style strong text. + * + * If not set, the renderer will attempt to infer the strong font from the + * regular text font. + */ +@property (nonatomic) NSDictionary *strongAttributes; + +/** + * Attributes used to style linked text. + * + * Defaults to using a blue foreground color and a single line underline style. + */ +@property (nonatomic) NSDictionary *linkAttributes; + +/** + * Attributes used to style code blocks. + * + * On iOS, defaults to the Menlo font when available, or Courier as a fallback. + * On OS X, defaults to the user monospaced font. + */ +@property (nonatomic) NSDictionary *codeBlockAttributes; + +/** + * Attributes used to style inline code. + * + * On iOS, defaults to the Menlo font when available, or Courier as a fallback. + * On OS X, defaults to the user monospaced font. + */ +@property (nonatomic) NSDictionary *inlineCodeAttributes; + +/** + * Attributes used to style block quotes. + * + * Defaults to using a paragraph style with a head indent of 30px. + */ +@property (nonatomic) NSDictionary *blockQuoteAttributes; + +/** + * Attributes used to style ordered lists. + * + * These attributes will apply to the entire list (unless overriden by attributes + * for the list items), including the numbers. + * + * Defaults to using a paragraph style with a head indent of 30px. + */ +@property (nonatomic) NSDictionary *orderedListAttributes; + +/** + * Attributes used to style unordered lists. + * + * These attributes will apply to the entire list (unless overriden by attributes + * for the list items), including the bullets. + * + * Defaults to using a paragraph style with a head indent of 30px. + */ +@property (nonatomic) NSDictionary *unorderedListAttributes; + +/** + * Attributes used to style ordered sublists. + * + * These attributes will apply to the entire list (unless overriden by attributes + * for the list items), including the numbers. + * + * Defaults to using a paragraph style with a head indent of 30px. + */ +@property (nonatomic) NSDictionary *orderedSublistAttributes; + +/** + * Attributes used to style unordered sublists. + * + * These attributes will apply to the entire list (unless overriden by attributes + * for the list items), including the bullets. + * + * Defaults to using a paragraph style with a head indent of 30px. + */ +@property (nonatomic) NSDictionary *unorderedSublistAttributes; + +/** + * Attributes used to style ordered list items. + * + * These attribtues do _not_ apply to the numbers. + */ +@property (nonatomic) NSDictionary *orderedListItemAttributes; + +/** + * Attributes used to style unordered list items. + * + * These attribtues do _not_ apply to the bullets. + */ +@property (nonatomic) NSDictionary *unorderedListItemAttributes; + +@end diff --git a/MarkDownEditor/Classes/CMTextAttributes.m b/MarkDownEditor/Classes/CMTextAttributes.m new file mode 100755 index 0000000..d838c62 --- /dev/null +++ b/MarkDownEditor/Classes/CMTextAttributes.m @@ -0,0 +1,185 @@ +// +// CMTextAttributes.m +// MarkDownEditor +// +// Created by Indragie on 1/15/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import "CMTextAttributes.h" +#import "CMPlatformDefines.h" + +static NSDictionary * CMDefaultTextAttributes() +{ +#if TARGET_OS_IPHONE + return @{NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleBody]}; +#else + return @{NSFontAttributeName: [NSFont userFontOfSize:12.0]}; +#endif +} + +static NSDictionary * CMDefaultH1Attributes() +{ +#if TARGET_OS_IPHONE + return @{NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]}; +#else + return @{NSFontAttributeName: [NSFont userFontOfSize:24.0]}; +#endif +} + +static NSDictionary * CMDefaultH2Attributes() +{ +#if TARGET_OS_IPHONE + return @{NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]}; +#else + return @{NSFontAttributeName: [NSFont userFontOfSize:18.0]}; +#endif +} + +static NSDictionary * CMDefaultH3Attributes() +{ +#if TARGET_OS_IPHONE + return @{NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]}; +#else + return @{NSFontAttributeName: [NSFont userFontOfSize:14.0]}; +#endif +} + +static NSDictionary * CMDefaultH4Attributes() +{ +#if TARGET_OS_IPHONE + return @{NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]}; +#else + return @{NSFontAttributeName: [NSFont userFontOfSize:12.0]}; +#endif +} + +static NSDictionary * CMDefaultH5Attributes() +{ +#if TARGET_OS_IPHONE + return @{NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]}; +#else + return @{NSFontAttributeName: [NSFont userFontOfSize:10.0]}; +#endif +} + +static NSDictionary * CMDefaultH6Attributes() +{ +#if TARGET_OS_IPHONE + return @{NSFontAttributeName: [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]}; +#else + return @{NSFontAttributeName: [NSFont userFontOfSize:8.0]}; +#endif +} + +static NSDictionary * CMDefaultLinkAttributes() +{ + return @{ +#if TARGET_OS_IPHONE + NSForegroundColorAttributeName: UIColor.blueColor, +#else + NSForegroundColorAttributeName: NSColor.blueColor, +#endif + NSUnderlineStyleAttributeName: @(NSUnderlineStyleSingle) + }; +} + +#if TARGET_OS_IPHONE +static UIFont * MonospaceFont() +{ + CGFloat size = [[UIFont preferredFontForTextStyle:UIFontTextStyleBody] pointSize]; + return [UIFont fontWithName:@"Menlo" size:size] ?: [UIFont fontWithName:@"Courier" size:size]; +} +#endif + +static NSParagraphStyle * DefaultIndentedParagraphStyle() +{ + NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; + style.firstLineHeadIndent = 30; + style.headIndent = 30; + return style; +} + +static NSDictionary * CMDefaultCodeBlockAttributes() +{ + return @{ +#if TARGET_OS_IPHONE + NSFontAttributeName: MonospaceFont(), +#else + NSFontAttributeName: [NSFont userFixedPitchFontOfSize:12.0], +#endif + NSParagraphStyleAttributeName: DefaultIndentedParagraphStyle() + }; +} + +static NSDictionary * CMDefaultInlineCodeAttributes() +{ +#if TARGET_OS_IPHONE + return @{NSFontAttributeName: MonospaceFont()}; +#else + return @{NSFontAttributeName: [NSFont userFixedPitchFontOfSize:12.0]}; +#endif +} + +static NSDictionary * CMDefaultBlockQuoteAttributes() +{ + return @{NSParagraphStyleAttributeName: DefaultIndentedParagraphStyle()}; +} + +static NSDictionary * CMDefaultOrderedListAttributes() +{ + return @{NSParagraphStyleAttributeName: DefaultIndentedParagraphStyle()}; +} + +static NSDictionary * CMDefaultUnorderedListAttributes() +{ + return @{NSParagraphStyleAttributeName: DefaultIndentedParagraphStyle()}; +} + +static NSDictionary * CMDefaultOrderedSublistAttributes() +{ + return @{NSParagraphStyleAttributeName: DefaultIndentedParagraphStyle()}; +} + +static NSDictionary * CMDefaultUnorderedSublistAttributes() +{ + return @{NSParagraphStyleAttributeName: DefaultIndentedParagraphStyle()}; +} + +@implementation CMTextAttributes + +- (instancetype)init +{ + if ((self = [super init])) { + _textAttributes = CMDefaultTextAttributes(); + _h1Attributes = CMDefaultH1Attributes(); + _h2Attributes = CMDefaultH2Attributes(); + _h3Attributes = CMDefaultH3Attributes(); + _h4Attributes = CMDefaultH4Attributes(); + _h5Attributes = CMDefaultH5Attributes(); + _h6Attributes = CMDefaultH6Attributes(); + _linkAttributes = CMDefaultLinkAttributes(); + _codeBlockAttributes = CMDefaultCodeBlockAttributes(); + _inlineCodeAttributes = CMDefaultInlineCodeAttributes(); + _blockQuoteAttributes = CMDefaultBlockQuoteAttributes(); + _orderedListAttributes = CMDefaultOrderedListAttributes(); + _unorderedListAttributes = CMDefaultUnorderedListAttributes(); + _orderedSublistAttributes = CMDefaultOrderedSublistAttributes(); + _unorderedSublistAttributes = CMDefaultUnorderedSublistAttributes(); + } + return self; +} + +- (NSDictionary *)attributesForHeaderLevel:(NSInteger)level +{ + switch (level) { + case 1: return _h1Attributes; + case 2: return _h2Attributes; + case 3: return _h3Attributes; + case 4: return _h4Attributes; + case 5: return _h5Attributes; + default: return _h6Attributes; + } +} + +@end diff --git a/MarkDownEditor/Classes/MarkDownEditor.h b/MarkDownEditor/Classes/MarkDownEditor.h new file mode 100755 index 0000000..4194520 --- /dev/null +++ b/MarkDownEditor/Classes/MarkDownEditor.h @@ -0,0 +1,29 @@ +// +// MarkDownEditor +// MarkDownEditor +// +// Created by Indragie on 1/12/15. +// Copyright (c) 2015 Indragie Karunaratne. All rights reserved. +// + +#import + +//! Project version number for MarkDownEditor. +FOUNDATION_EXPORT double MarkDownEditorVersionNumber; + +//! Project version string for MarkDownEditor. +FOUNDATION_EXPORT const unsigned char MarkDownEditorVersionString[]; + +#import "CMAttributedStringRenderer.h" +#import "CMDocument.h" +#import "CMDocument+AttributedStringAdditions.h" +#import "CMDocument+HTMLAdditions.h" +#import "CMHTMLRenderer.h" +#import "CMHTMLStrikethroughTransformer.h" +#import "CMHTMLUnderlineTransformer.h" +#import "CMHTMLSuperscriptTransformer.h" +#import "CMHTMLSubscriptTransformer.h" +#import "CMIterator.h" +#import "CMNode.h" +#import "CMParser.h" +#import "CMTextAttributes.h" diff --git a/README.md b/README.md new file mode 100644 index 0000000..96662cd --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# MarkDownEditor + +[![CI Status](http://img.shields.io/travis/Cu-Toof/MarkDownEditor.svg?style=flat)](https://travis-ci.org/Cu-Toof/MarkDownEditor) +[![Version](https://img.shields.io/cocoapods/v/MarkDownEditor.svg?style=flat)](http://cocoapods.org/pods/MarkDownEditor) +[![License](https://img.shields.io/cocoapods/l/MarkDownEditor.svg?style=flat)](http://cocoapods.org/pods/MarkDownEditor) +[![Platform](https://img.shields.io/cocoapods/p/MarkDownEditor.svg?style=flat)](http://cocoapods.org/pods/MarkDownEditor) + +## Example + +To run the example project, clone the repo, and run `pod install` from the Example directory first. + +## Requirements + +## Installation + +MarkDownEditor is available through [CocoaPods](http://cocoapods.org). To install +it, simply add the following line to your Podfile: + +```ruby +pod "MarkDownEditor" +``` + +## Author + +Cu-Toof, toanf9dn@gmail.com + +## License + +MarkDownEditor is available under the MIT license. See the LICENSE file for more info. diff --git a/_Pods.xcodeproj b/_Pods.xcodeproj new file mode 120000 index 0000000..3c5a8e7 --- /dev/null +++ b/_Pods.xcodeproj @@ -0,0 +1 @@ +Example/Pods/Pods.xcodeproj \ No newline at end of file