diff --git a/MMMarkdown.xcodeproj/project.pbxproj b/MMMarkdown.xcodeproj/project.pbxproj index 7bfb7aa..aa2a096 100644 --- a/MMMarkdown.xcodeproj/project.pbxproj +++ b/MMMarkdown.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + B9FD4F131DE8D48E00DD679D /* MMChecklistTests.m in Sources */ = {isa = PBXBuildFile; fileRef = B9FD4F121DE8D48E00DD679D /* MMChecklistTests.m */; }; BE23E8FC1C0CF4190087F587 /* MMMarkdown.h in Headers */ = {isa = PBXBuildFile; fileRef = BE813DE214F893EB00EC9469 /* MMMarkdown.h */; }; BE23E8FD1C0CF4190087F587 /* MMMarkdown.m in Sources */ = {isa = PBXBuildFile; fileRef = BE813DE314F893EB00EC9469 /* MMMarkdown.m */; }; BE23E8FE1C0CF4190087F587 /* MMMarkdown-Prefix.pch in Headers */ = {isa = PBXBuildFile; fileRef = BE813DE114F893EB00EC9469 /* MMMarkdown-Prefix.pch */; }; @@ -272,6 +273,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + B9FD4F121DE8D48E00DD679D /* MMChecklistTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MMChecklistTests.m; sourceTree = ""; }; BE1DB5D41514184000B26092 /* MMSpanTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MMSpanTests.m; sourceTree = ""; }; BE1DB5D915143F4200B26092 /* MMBlockTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MMBlockTests.m; sourceTree = ""; }; BE1DB5DC1515032F00B26092 /* MMLinkTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MMLinkTests.m; sourceTree = ""; }; @@ -497,6 +499,7 @@ BEAAEB421549DD1700EF1EA0 /* MMImageTests.m */, BE1DB5DC1515032F00B26092 /* MMLinkTests.m */, BE813E9A15031B4000EC9469 /* MMListTests.m */, + B9FD4F121DE8D48E00DD679D /* MMChecklistTests.m */, BE1DB5D41514184000B26092 /* MMSpanTests.m */, BE813DF714F893EB00EC9469 /* MMMarkdownTests.h */, BE813DF814F893EB00EC9469 /* MMMarkdownTests.m */, @@ -913,6 +916,7 @@ BED761871B37783100687B36 /* MMImageTests.m in Sources */, BED761821B37783100687B36 /* MMBlockTests.m in Sources */, BED7618C1B37783100687B36 /* MMPHPMarkdownTests.m in Sources */, + B9FD4F131DE8D48E00DD679D /* MMChecklistTests.m in Sources */, BED7618B1B37783100687B36 /* MMMarkdownTests.m in Sources */, BED761851B37783100687B36 /* MMExtensionTests.m in Sources */, ); diff --git a/Source/MMElement.h b/Source/MMElement.h index 5492d74..4292b35 100644 --- a/Source/MMElement.h +++ b/Source/MMElement.h @@ -36,6 +36,12 @@ typedef enum MMElementTypeNumberedList, MMElementTypeBulletedList, MMElementTypeListItem, + MMElementTypeMinusChecklist, + MMElementTypePlusChecklist, + MMElementTypeAsteriskChecklist, + MMElementTypeNumberedChecklist, + MMElementTypeChecklistCheckedItem, + MMElementTypeChecklistUncheckedItem, MMElementTypeCodeBlock, MMElementTypeHorizontalRule, MMElementTypeHTML, diff --git a/Source/MMGenerator.m b/Source/MMGenerator.m index 810015f..9777213 100644 --- a/Source/MMGenerator.m +++ b/Source/MMGenerator.m @@ -92,6 +92,18 @@ return @"
    \n"; case MMElementTypeListItem: return @"
  1. "; + case MMElementTypeMinusChecklist: + return @"
      \n"; + case MMElementTypePlusChecklist: + return @"
        \n"; + case MMElementTypeAsteriskChecklist: + return @"
          \n"; + case MMElementTypeNumberedChecklist: + return @"
            \n"; + case MMElementTypeChecklistCheckedItem: + return @"
          1. "; + case MMElementTypeChecklistUncheckedItem: + return @"
          2. "; case MMElementTypeBlockquote: return @"
            \n"; case MMElementTypeCodeBlock: @@ -167,6 +179,15 @@ return @"
          \n"; case MMElementTypeListItem: return @"\n"; + case MMElementTypeMinusChecklist: + case MMElementTypePlusChecklist: + case MMElementTypeAsteriskChecklist: + return @"
        \n"; + case MMElementTypeNumberedChecklist: + return @"
\n"; + case MMElementTypeChecklistCheckedItem: + case MMElementTypeChecklistUncheckedItem: + return @"\n"; case MMElementTypeBlockquote: return @"\n"; case MMElementTypeCodeBlock: diff --git a/Source/MMMarkdown.h b/Source/MMMarkdown.h index 08b02a3..7251f3e 100644 --- a/Source/MMMarkdown.h +++ b/Source/MMMarkdown.h @@ -45,8 +45,9 @@ typedef NS_OPTIONS(NSUInteger, MMMarkdownExtensions) // MMMarkdownExtensionsTableCaptions = 1 << 7, MMMarkdownExtensionsTables = 1 << 8, MMMarkdownExtensionsUnderscoresInWords = 1 << 9, + MMMarkdownExtensionsTaskList = 1 << 10, // https://github.com/blog/1375-task-lists-in-gfm-issues-pulls-comments - MMMarkdownExtensionsGitHubFlavored = MMMarkdownExtensionsAutolinkedURLs|MMMarkdownExtensionsFencedCodeBlocks|MMMarkdownExtensionsHardNewlines|MMMarkdownExtensionsStrikethroughs|MMMarkdownExtensionsTables|MMMarkdownExtensionsUnderscoresInWords, + MMMarkdownExtensionsGitHubFlavored = MMMarkdownExtensionsAutolinkedURLs|MMMarkdownExtensionsFencedCodeBlocks|MMMarkdownExtensionsHardNewlines|MMMarkdownExtensionsStrikethroughs|MMMarkdownExtensionsTables|MMMarkdownExtensionsUnderscoresInWords|MMMarkdownExtensionsTaskList, }; NS_ASSUME_NONNULL_BEGIN diff --git a/Source/MMParser.m b/Source/MMParser.m index 663157b..05e3959 100644 --- a/Source/MMParser.m +++ b/Source/MMParser.m @@ -240,6 +240,22 @@ - (MMElement *)_parseBlockElementWithScanner:(MMScanner *)scanner if (element) return element; + // checklist has more sophisticated leading structure + // it trumps code block. + if (self.extensions & MMMarkdownExtensionsTaskList) + { + // check list - github flavored markdown: + // + // - [ ] check item 1 + // - [X] check item 2 + // + [scanner beginTransaction]; + element = [self _parseChecklistWithScanner:scanner]; + [scanner commitTransaction:element != nil]; + if (element) + return element; + } + // Check code first because its four-space behavior trumps most else [scanner beginTransaction]; element = [self _parseCodeBlockWithScanner:scanner]; @@ -710,6 +726,378 @@ - (MMElement *)_parseHorizontalRuleWithScanner:(MMScanner *)scanner return element; } + +#pragma mark - Checklist + + +/*! + Check the following tasklist markers: + + - [ ] text + - [x] text + - [X] text + + [ ] text + * [ ] text + 1. [ ] text + + + @param scanner + A MMScanner, Must not be nil. + @param type + A list type enum pointer. If found a valid tasklist marker, the type value will be save into this point. + @param expectedListType + Expected type enum. MMElementTypeNone value means that we are expecting any valid tasklist marker + @param checked + A nullable boolean pointer. If found a valid tasklist marker, the tasklist checking status will be save into this point. + @result + Returns a boolean value. ture if found a valid tasklist marker, false otherwise. + */ +- (BOOL)_parseTasklistMarkerWithScanner:(MMScanner *)scanner listType:(MMElementType*)type expectedListType:(MMElementType)expectedListType checked:(nullable BOOL*)checked +{ + // checking unordered list marker + if (expectedListType != MMElementTypeNone && + expectedListType != MMElementTypeMinusChecklist && + expectedListType != MMElementTypePlusChecklist && + expectedListType != MMElementTypeAsteriskChecklist && + expectedListType != MMElementTypeNumberedChecklist) + return NO; + + [scanner beginTransaction]; + unichar nextChar = scanner.nextCharacter; + unichar fistChar = nextChar; + if (nextChar != '*' && nextChar != '-' && nextChar != '+' && !isdigit(nextChar)) + { + [scanner commitTransaction:NO]; + return NO; + } + + if (isdigit(nextChar)) { + do{ + [scanner advance]; + nextChar = scanner.nextCharacter; + } while(isdigit(nextChar)); + + if(nextChar != '.') + { + [scanner commitTransaction:NO]; + return NO; + } + } + + [scanner advance]; + nextChar = scanner.nextCharacter; + if (nextChar != ' ') + { + [scanner commitTransaction:NO]; + return NO; + } + + + [scanner advance]; + nextChar = scanner.nextCharacter; + if (nextChar != '[') + { + [scanner commitTransaction:NO]; + return NO; + } + + + [scanner advance]; + nextChar = scanner.nextCharacter; + if (nextChar != ' ' && nextChar != 'X' && nextChar != 'x') + { + [scanner commitTransaction:NO]; + return NO; + } + + BOOL foundx = nextChar == 'X' || nextChar == 'x'; + [scanner advance]; + nextChar = scanner.nextCharacter; + if (nextChar != ']') + { + [scanner commitTransaction:NO]; + return NO; + } + + [scanner advance]; + nextChar = scanner.nextCharacter; + if (nextChar != ' ') + { + [scanner commitTransaction:NO]; + return NO; + } + + if(fistChar == '-') + { + *type = MMElementTypeMinusChecklist; + } + else if(fistChar == '+') + { + *type = MMElementTypePlusChecklist; + } + else if(fistChar == '*') + { + *type = MMElementTypeAsteriskChecklist; + } + else{ + *type = MMElementTypeNumberedChecklist; + } + + if(checked != NULL){ + *checked = foundx; + } + + [scanner commitTransaction:YES]; + + return YES; +} + + + +- (MMElement *)_parseTasklistItemWithScanner:(MMScanner *)scanner listType:(MMElementType*)listType expectedListType:(MMElementType)thelistType +{ + BOOL canContainBlocks = NO; + + if ([scanner skipEmptyLines]) + { + canContainBlocks = YES; + } + + [scanner skipIndentationUpTo:7]; // Optional space + MMElementType thislistType = MMElementTypeNone; + BOOL checked = NO; + BOOL foundAnItem = [self _parseTasklistMarkerWithScanner:scanner listType:&thislistType expectedListType:thelistType checked:&checked]; + if (!foundAnItem) + return nil; + + *listType = thislistType; + + [scanner skipWhitespace]; + + MMElement *element = [MMElement new]; + element.type = checked ? MMElementTypeChecklistCheckedItem : MMElementTypeChecklistUncheckedItem; + + BOOL afterBlankLine = NO; + NSUInteger nestedListIndex = NSNotFound; + NSUInteger nestedListIndentation = 0; + while (!scanner.atEndOfString) + { + // Skip over any empty lines + [scanner beginTransaction]; + NSUInteger numOfEmptyLines = [scanner skipEmptyLines]; + afterBlankLine = numOfEmptyLines != 0; + + // Check for a horizontal rule + [scanner beginTransaction]; + BOOL newRule = [self _parseHorizontalRuleWithScanner:scanner] != nil; + [scanner commitTransaction:NO]; + if (newRule) + { + [scanner commitTransaction:NO]; + break; + } + + // Check for the start of a new list item + [scanner beginTransaction]; + [scanner skipIndentationUpTo:1]; + BOOL newMarker = [self _parseTasklistMarkerWithScanner:scanner listType:&thislistType expectedListType:thelistType checked:NULL]; + [scanner commitTransaction:NO]; + if (newMarker) + { + [scanner commitTransaction:NO]; + if (afterBlankLine) + { + canContainBlocks = YES; + } + break; + } + + // Check for a nested list + [scanner beginTransaction]; + NSUInteger indentation = [scanner skipIndentationUpTo:4]; + [scanner beginTransaction]; + BOOL newList = [self _parseTasklistMarkerWithScanner:scanner listType:&thislistType expectedListType:thelistType checked:NULL]; + [scanner commitTransaction:NO]; + if (indentation >= 2 && newList && nestedListIndex == NSNotFound) + { + [element addInnerRange:NSMakeRange(scanner.location, 0)]; + nestedListIndex = element.innerRanges.count; + [element addInnerRange:scanner.currentRange]; + + [scanner commitTransaction:YES]; + [scanner commitTransaction:YES]; + [scanner advanceToNextLine]; + nestedListIndentation = indentation; + continue; + } + [scanner commitTransaction:NO]; + + if (afterBlankLine) + { + // Must be 4 spaces past the indentation level to start a new paragraph + [scanner beginTransaction]; + NSUInteger indentation = [scanner skipIndentationUpTo:4]; + if (indentation < 4) + { + [scanner commitTransaction:NO]; + [scanner commitTransaction:NO]; + break; + } + [scanner commitTransaction:YES]; + [scanner commitTransaction:YES]; + + [element addInnerRange:NSMakeRange(scanner.location, 0)]; + canContainBlocks = YES; + } + else + { + [scanner commitTransaction:YES]; + + // Don't skip past where a nested list would start because that list + // could have its own nested list, so the whitespace will be needed. + [scanner skipIndentationUpTo:nestedListIndentation]; + } + + if (nestedListIndex != NSNotFound) + { + [element addInnerRange:scanner.currentRange]; + [scanner advanceToNextLine]; + } + else + { + [self _addTextLineToElement:element withScanner:scanner]; + } + + [scanner beginTransaction]; + [scanner skipIndentationUpTo:4]; + + // + // checklist will ignore blockquote rules + // + // if (scanner.nextCharacter == '>') + // { + // // + // // If next line is start with blockquote mark + // // then break current list parsering. + // // + // // for example: + // // + // // > 123 + // // + abc + // // + // // "+ abs" should not consider as part of blockquote + // // + // // > 234 + // // 567 + // // + // // "567" is part of the blockquote + // // + // [scanner commitTransaction:NO]; + // break; + // } + // + + [scanner commitTransaction:NO]; + + } + + element.range = NSMakeRange(scanner.startLocation, scanner.location-scanner.startLocation); + + if (element.innerRanges.count > 0) + { + if (nestedListIndex != NSNotFound) + { + NSArray *preListRanges = [element.innerRanges subarrayWithRange:NSMakeRange(0, nestedListIndex)]; + NSArray *postListRanges = [element.innerRanges subarrayWithRange:NSMakeRange(nestedListIndex, element.innerRanges.count - nestedListIndex)]; + MMScanner *preListScanner = [MMScanner scannerWithString:scanner.string lineRanges:preListRanges]; + MMScanner *postListScanner = [MMScanner scannerWithString:scanner.string lineRanges:postListRanges]; + + if (canContainBlocks) + { + element.children = [self _parseElementsWithScanner:preListScanner]; + } + else + { + element.children = [self.spanParser parseSpansInBlockElement:element withScanner:preListScanner]; + } + + element.children = [element.children arrayByAddingObjectsFromArray:[self _parseElementsWithScanner:postListScanner]]; + } + else + { + MMScanner *innerScanner = [MMScanner scannerWithString:scanner.string lineRanges:element.innerRanges]; + if (canContainBlocks) + { + element.children = [self _parseElementsWithScanner:innerScanner]; + } + else + { + element.children = [self.spanParser parseSpansInBlockElement:element withScanner:innerScanner]; + } + } + } + + return element; +} + + + +- (MMElement *)_parseChecklistWithScanner:(MMScanner *)scanner +{ + [scanner beginTransaction]; + [scanner skipIndentationUpTo:7]; // checklist allow optional 7 leading space or 1 tab. above that it's code block. + MMElementType listType = MMElementTypeNone; + MMElementType nextListType = MMElementTypeNone; + + BOOL hasMarker = [self _parseTasklistMarkerWithScanner:scanner listType:&listType expectedListType:MMElementTypeNone checked:NULL]; + [scanner commitTransaction:NO]; + + + if (!hasMarker) + return nil; + + MMElement *element = [MMElement new]; + element.type = listType; + + while (!scanner.atEndOfString) + { + [scanner beginTransaction]; + + // Check for a horizontal rule first -- they look like a list marker + [scanner skipEmptyLines]; + MMElement *rule = [self _parseHorizontalRuleWithScanner:scanner]; + + [scanner commitTransaction:NO]; + if (rule) + break; + + [scanner beginTransaction]; + MMElement *item = [self _parseTasklistItemWithScanner:scanner listType: &nextListType expectedListType:listType]; + if (!item) + { + [scanner commitTransaction:NO]; + break; + } + + if(nextListType != listType) + { + [scanner commitTransaction:NO]; + break; + } + + [scanner commitTransaction:YES]; + + [element addChild:item]; + } + + element.range = NSMakeRange(scanner.startLocation, scanner.location-scanner.startLocation); + + return element; +} + + +#pragma mark - List + - (BOOL)_parseListMarkerWithScanner:(MMScanner *)scanner listType:(MMListType)listType { switch (listType) @@ -932,6 +1320,7 @@ - (MMElement *)_parseListItemWithScanner:(MMScanner *)scanner listType:(MMListTy return element; } + - (MMElement *)_parseListWithScanner:(MMScanner *)scanner { [scanner beginTransaction]; diff --git a/Tests/MMChecklistTests.m b/Tests/MMChecklistTests.m new file mode 100644 index 0000000..7b08f91 --- /dev/null +++ b/Tests/MMChecklistTests.m @@ -0,0 +1,317 @@ +// +// MMChecklistTests.m +// MMMarkdown +// +// Created by Xu Lian on 25/11/2016. +// +// + +#import +#import "MMTestCase.h" + +@interface MMChecklistTests : XCTestCase + +@end + +@implementation MMChecklistTests + +#pragma mark - Tests + +- (void)testBasicChecklist_bulletedWithDashes +{ + NSString *markdown = @"- [ ] One\n" + "- [ ] Two\n" + "- [ ] Three\n"; + NSString *html = @"
  • One
  • Two
  • Three
"; + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList ,markdown, html); +} + + +- (void)testBasicChecklist_bulletedWithStars +{ + NSString *markdown = @"* [ ] One\n" + "* [ ] Two\n" + "* [ ] Three\n"; + NSString *html = @"
  • One
  • Two
  • Three
"; + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList ,markdown, html); +} + + +- (void)testBasicChecklist_numbered +{ + NSString *markdown = @"0. [ ] One\n" + "1. [ ] Two\n" + "2. [ ] Three\n"; + NSString *html = @"
  1. One
  2. Two
  3. Three
"; + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList ,markdown, html); +} + + +- (void)testBasicChecklist_bulletedWithDashes_checked +{ + NSString *markdown = @"- [X] One\n" + "- [x] Two\n" + "- [ ] Three\n"; + NSString *html = @"
  • One
  • Two
  • Three
"; + + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList ,markdown, html); +} + + +- (void)testBasicChecklist_bulletedWithStars_checked +{ + NSString *markdown = @"* [ ] One\n" + "* [x] Two\n" + "* [X] Three\n"; + NSString *html = @"
  • One
  • Two
  • Three
"; + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList ,markdown, html); +} + + +- (void)testBasicChecklist_numbered_checked +{ + NSString *markdown = @"0. [x] One\n" + "1. [ ] Two\n" + "2. [X] Three\n"; + NSString *html = @"
  1. One
  2. Two
  3. Three
"; + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList ,markdown, html); +} + + +- (void)testBasicChecklist_bulletedWithParagraphs +{ + NSString *markdown = @"- [ ] One\n" + "\n" + "- [ ] Two\n" + "\n" + "- [ ] Three\n"; + NSString *html = @"
  • One

  • " + "
  • Two

  • " + "
  • Three

"; + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList ,markdown, html); +} + + +- (void)testChecklist_carriageReturn +{ + NSString *markdown = @"- [ ] One\r- [ ] Two\r"; + NSString *html = @"
  • One
  • \n
  • Two
"; + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, markdown, html); +} + + +- (void)testInvalidChecklist_emptylist +{ + NSString *markdown = @"- [ ]\n- [ ]\n- [ ]\n"; + NSString *html = @"
    \n
  • [ ]
  • \n
  • [ ]
  • \n
  • [ ]
  • \n
"; + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, markdown, html); +} + +- (void)testValidChecklist_emptylist +{ + NSString *markdown = @"- [ ] \n- [ ] \n- [ ] \n"; + NSString *html = @"
  • \n
"; + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, markdown, html); +} + +- (void)testNestedChecklists +{ + NSString *markdown = @"- [ ] 1\n" + " - [ ] A\n" + " - [ ] B\n" + "- [ ] 2\n" + "- [ ] 3\n"; + + NSString *html = @"
  • 1\n
    • A
    • B
  • " + "
  • 2
  • " + "
  • 3
"; + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, markdown, html); +} + + +- (void)testNestedChecklists_checked +{ + NSString *markdown = @"- [ ] 1\n" + " - [X] A\n" + " - [x] B\n" + "- [ ] 2\n" + "- [ ] 3\n"; + + NSString *html = @"
  • 1\n
    • A
    • B
  • " + "
  • 2
  • " + "
  • 3
"; + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, markdown, html); +} + + + +- (void)testNestedChecklists_multipleLevels_checked +{ + NSString *markdown = @"- [ ] 1\n" + " - [X] A\n" + " - [ ] X\n" + " - [X] Y\n" + " - [x] B\n" + "- [ ] 2\n" + "- [ ] 3\n"; + + NSString *html = @"
    " + "
  • 1\n" + "
      " + "
    • A\n" + "
        " + "
      • X
      • " + "
      • Y
      • " + "
      " + "
    • " + "
    • B
    • " + "
    " + "
  • " + "
  • 2
  • " + "
  • 3
"; + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, markdown, html); +} + + +- (void)testChecklist_followedByHorizontalRule +{ + NSString *markdown = @"* [ ] One\n" + "* [ ] Two\n" + "* [ ] Three\n" + "\n" + " * * * "; + NSString *html = @"
  • One
  • " + "
  • Two
  • " + "
  • Three
" + "
"; + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, markdown, html); +} + +- (void)testChecklist_invalidMarker +{ + // First element + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"*[]One\n", @"

*[]One

"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"-[]One\n", @"

-[]One

"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"+[]One\n", @"

+[]One

"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"1.[]One\n", @"

1.[]One

"); + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"* []One\n", @"
  • []One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"- []One\n", @"
  • []One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"+ []One\n", @"
  • []One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"1. []One\n", @"
  1. []One
"); + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"*[ ]One\n", @"

*[ ]One

"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"-[ ]One\n", @"

-[ ]One

"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"+[ ]One\n", @"

+[ ]One

"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"1.[ ]One\n", @"

1.[ ]One

"); + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"*[] One\n", @"

*[] One

"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"-[] One\n", @"

-[] One

"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"+[] One\n", @"

+[] One

"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"1.[] One\n", @"

1.[] One

"); + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"* [ ]One\n", @"
  • [ ]One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"- [ ]One\n", @"
  • [ ]One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"+ [ ]One\n", @"
  • [ ]One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"1. [ ]One\n", @"
  1. [ ]One
"); + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"* [] One\n", @"
  • [] One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"- [] One\n", @"
  • [] One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"+ [] One\n", @"
  • [] One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"1. [] One\n", @"
  1. [] One
"); + + // Second element + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"*[ ] One\n*Two", @"

*[ ] One\n*Two

"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"-[ ] One\n-Two", @"

-[ ] One\n-Two

"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"+[ ] One\n+Two", @"

+[ ] One\n+Two

"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"1.[ ] One\n1.Two", @"

1.[ ] One\n1.Two

"); + + // Check with tabs + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"*\t[]One\n", @"
  • []One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"-\t[]One\n", @"
  • []One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"+\t[]One\n", @"
  • []One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"1.\t[]One\n", @"
  1. []One
"); + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"*\t[ ] One\n", @"
  • [ ] One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"-\t[ ] One\n", @"
  • [ ] One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"+\t[ ] One\n", @"
  • [ ] One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"1.\t[ ] One\n", @"
  1. [ ] One
"); + + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"1. [\t] One\n", @"
  1. [ ] One
"); + +} + + +- (void)testChecklist_validMarker +{ + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"* [ ]\tOne\n", @"
  • One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"- [ ]\tOne\n", @"
  • One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"+ [ ]\tOne\n", @"
  • One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"1. [ ]\tOne\n", @"
  1. One
"); + +#warning these suppose to be invalid checklist items according to Github + + // + // these suppose to be invalid checklist items + // but seems that _removeTabsFromString function made it works. + // + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"* [\t] One\n", @"
  • One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"- [\t] One\n", @"
  • One
"); + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"+ [\t] One\n", @"
  • One
"); +} + + +- (void)testChecklist_withLeadingSpace +{ + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @" - [ ] One\n - [ ] Two", @"
  • One
  • Two
"); +} + +- (void)testChecklist_withBold +{ + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"- [ ] One **Bold**\n- [ ] Two", @"
  • One Bold
  • Two
"); +} + +- (void)testChecklist_withCode +{ + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, @"- [ ] One `Code`\n- [ ] Two", @"
  • One Code
  • Two
"); +} + + +- (void)testChecklistFollowingAnotherChecklist +{ + NSString *markdown = + @"- [ ] A\n" + "- [ ] B\n" + "\n" + "1. [ ] 1\n" + "1. [ ] 2\n"; + NSString *HTML = + @"
    " + "
  • A
  • " + "
  • B

  • " + "
" + "
    " + "
  1. 1
  2. " + "
  3. 2
  4. " + "
"; + + MMAssertExtendedMarkdownEqualsHTML(MMMarkdownExtensionsTaskList, markdown, HTML); +} + +@end diff --git a/Tests/MMExtensionTests.m b/Tests/MMExtensionTests.m index e903169..3c115d6 100644 --- a/Tests/MMExtensionTests.m +++ b/Tests/MMExtensionTests.m @@ -26,24 +26,6 @@ #import "MMTestCase.h" -#define MMAssertExtendedMarkdownEqualsHTML(_extensions, markdown, html) \ - do { \ - id a1value = (markdown); \ - id a2value = (html); \ - \ - NSError *error; \ - NSString *output = [MMMarkdown HTMLStringWithMarkdown:a1value extensions:(_extensions) error:&error]; \ - NSString *html2 = a2value;\ - \ - /* Add root elements for parsing */ \ - output = [NSString stringWithFormat:@"%@", output]; \ - html2 = [NSString stringWithFormat:@"%@", html2]; \ - \ - NSXMLDocument *actual = [[NSXMLDocument alloc] initWithXMLString:output options:0 error:nil]; \ - NSXMLDocument *expected = [[NSXMLDocument alloc] initWithXMLString:html2 options:0 error:nil]; \ - XCTAssertEqualObjects(actual, expected); \ - } while(0) - @interface MMGitHubTests : MMTestCase @end diff --git a/Tests/MMTestCase.h b/Tests/MMTestCase.h index 9f30e31..2cc80f6 100644 --- a/Tests/MMTestCase.h +++ b/Tests/MMTestCase.h @@ -65,6 +65,30 @@ XCTAssertEqualObjects(actual, expected); \ } while(0) + +#define MMAssertExtendedMarkdownEqualsHTML(_extensions, markdown, html) \ + do { \ + id a1value = (markdown); \ + id a2value = (html); \ + \ + NSError *error; \ + NSString *output = [MMMarkdown HTMLStringWithMarkdown:a1value extensions:(_extensions) error:&error]; \ + NSString *html2 = a2value;\ + \ + /* Add root elements for parsing */ \ + output = [NSString stringWithFormat:@"%@", output]; \ + html2 = [NSString stringWithFormat:@"%@", html2]; \ + \ + NSError *actualError; \ + NSError *expectedError; \ + NSXMLDocument *actual = [[NSXMLDocument alloc] initWithXMLString:output options:0 error:&actualError]; \ + NSXMLDocument *expected = [[NSXMLDocument alloc] initWithXMLString:html2 options:0 error:&expectedError]; \ + XCTAssertNotNil(actual, "%@", actualError); \ + XCTAssertNotNil(expected, "%@", expectedError); \ + XCTAssertEqualObjects(actual, expected); \ + } while(0) + + @interface MMTestCase : XCTestCase - (NSString *)stringWithContentsOfFile:(NSString *)aString inDirectory:(NSString *)aDirectory;