DTMarkdownParser – Cocoanetics https://www.cocoanetics.com Our DNA is written in Swift Thu, 26 May 2016 08:56:39 +0000 en-US hourly 1 https://wordpress.org/?v=6.4.3 39982308 DTMarkdownParser 0.2.2 https://www.cocoanetics.com/2016/05/dtmarkdownparser-0-2-2/ https://www.cocoanetics.com/2016/05/dtmarkdownparser-0-2-2/#respond Thu, 26 May 2016 08:56:39 +0000 https://www.cocoanetics.com/?p=10233 DTMarkdownParser is a sequential parser for markdown, with a similar sequential paradigm as NSXMLParser. I started this project in 2013 as a training case for TDD and going for 100% code coverage by unit tests.

Apparently this nifty little project of mine has some fans, so we are publishing a new release today to include all the improvements that were made in that 2.5 years… 😉

Changes

  • Jan Weiß fixed a HTML error in the SimpleHTMLGenerator Demo. He also converted the unit tests to XCTest.
  • Mikkel Selsøe Sorensen added the option to ignore URLs.
  • I fixed some warnings and carried out project updates recommended by Xcode 7.3

The update is tagged on GitHub and also available via Cocoapods.

]]>
https://www.cocoanetics.com/2016/05/dtmarkdownparser-0-2-2/feed/ 0 10233
DTMarkdownParser 0.2.0 https://www.cocoanetics.com/2013/11/dtmarkdownparser-0-2-0/ https://www.cocoanetics.com/2013/11/dtmarkdownparser-0-2-0/#comments Sun, 03 Nov 2013 13:23:51 +0000 http://www.cocoanetics.com/?p=8813 DTMarkdownParser, our event-based parser for Markdown, has much progressed since 0.1.0. Since I will not be able to work on this as much in the coming weeks and it has reached a nice stable state I’m releasing this new development version.

Changes

  • CHANGED: Rewritten and cleaned up parsing loop to be able to handle links or references spanning two lines
  • ADDED: Mac Demo
  • ADDED: Inline linking via angle brackets
  • ADDED: Auto-Linking of URLs and email addresses using NSDataDetector
  • FIXED: Many edge cases

There was lots of cleaning up around the main parsing loop that needed to be done. In the previous version the parsed string was divided into lines and these got processed. This was causing problems with some use cases where hyperlinks and references could not be handled if they were spanning two lines. So I rewrote the main parse loop from scratch.

Since it is required to know when parsing the beginning of a line there is an array of ranges that make up the individual lines. The initial implementation was using NSRange values wrapped in NSValue objects. Friend of the project Jan Weiß proposed an optimisation using a C-array which sped up the parsing quite a bit. This became DTRangesArray.

Jan also put much work into a very useful demo application. He can be reached via Twitter or his blog at Geheimwerk.de.

DTMarkdownParser Mac Demo

The demo allows you to load, view and edit markdown files in the left pane. In the right you see a tree view, the raw source or a web view of HTML that is generated from the DTMarkdownParser events. Having this useful app available let me find several bugs which otherwise I wouldn’t have seen.

There are still some features not yet implemented, which could use your help if you are interested:

  • handling of character escaping
  • stacking of blockquotes inside other blockquotes or list items.
  • wiring up of range reporting for syntax highlighting
  • a plethora of extended Markdown features

The update is tagged on the master branch of the GitHub repo as well as available via CocoaPods.

]]>
https://www.cocoanetics.com/2013/11/dtmarkdownparser-0-2-0/feed/ 7 8813
DTMarkdownParser 0.1.0 https://www.cocoanetics.com/2013/10/dtmarkdownparser-0-1-0/ https://www.cocoanetics.com/2013/10/dtmarkdownparser-0-1-0/#comments Wed, 23 Oct 2013 14:01:36 +0000 http://www.cocoanetics.com/?p=8791 My book publisher offers me two options for submitting text, Microsoft Word or an XML-based format. I’ve grown quite fond of Markdown lately and so I formed the idea that I could write my book in markdown and use a parser to create this XML from it.

And since I like to craft my own libraries (because most of the time I’m learning something in the process) I started to work on DTMarkdownParser.

I started work on the library with several design goals in mind:

  • Test-driven Development: fashion unit test cases for all the individual scenarios you could encounter in a markdown text and then implement the simplest possible way to get the unit test to pass. Rinse and repeat.
  • 100% coverage: set up Travis-CI and Coveralls to make sure that unit test coverage never decreases through pull requests and to also create tests for all possible branches in the parser core code
  • No external dependencies: Other projects often depend on libraries like the well-known discount library. But as I stated above this project is meant to be mainly an exercise for me and so depending on an external static lib is out of the question
  • Event-based API: similar to NSXMLParser I wanted to have events to be sent to a parsing delegate instead of generating HTML output. This would allow developers to hook up their own code to those events, or even use it as input for DTCoreText.
  • Collaborative: There are quite a few enhancements for Markdown and I depend on fellow developers to point these out – as needed – and where possible even implement them.

Unit-Testing a Delegate Protocol

Popular unit testing frameworks like OCMockito provide the ability to have an object pretend to be another. This is called “mocking”. The pretend-object is called a “mock”. You would use those objects in place of delegates to be able to test a specific class in isolation.

Typically you would mock a delegate object, set it as the delegate, call some code and then ask the mock if certain delegate methods were called with certain parameters. A variant of this theme is to specify the return value for specific mocked methods.

For DTMarkdownParser  I initially started out with OCMockito (which uses OCHamcrest) to mock delegates. But soon I realized that this would be too limited for me as there are several methods being called for most unit test cases. OCMockito – as far as I am aware – does not have a facility to tell you in which order methods have been called.

Consider the following example:

Hello Markdown. *This is emphasized*

This would result in the following methods being invoked on the delegate:

  1. Begin of document
  2. Begin of paragraph tag
  3. Found characters: “Hello Markdown. “
  4. Begin of EM tag
  5. Found characters. “This is emphasized”
  6. End of EM tag
  7. End of paragraph tag
  8. End of document

OCMockito would only allow me to inquire if there was a call to the method indicating that characters were found. But I couldn’t test the exact order.

And then there were also issues with getting OCMockito to work with code coverage on Travis-CI… so I removed it and switched back to SenTest.

Enter DTInvocationRecorder

So I built DTInvocationRecorder which can be configured to mock any kind of protocol. Then you set it as delegate. It records all invocations of all methods and you can then determine exactly what methods were called how.

In the unit test class I’m setting up the recorder like so:

- (void)setUp
{
   [super setUp];
 
   _recorder = [[DTInvocationRecorder alloc] init];
   [_recorder addProtocol:@protocol(DTMarkdownParserDelegate)];
}

The -addProtocol: method lets you specify protocols that it should pretend to be implementing. From the protocol it derives all necessary instance methods and will record only these. Any other method would produce an unrecognized selector exception.

Since I want to reset the contents of the recorder for each test case I’m clearing the table of recorded invocation before each test is executed.

- (void)performTest:(SenTestRun *)aRun
{
   // clear recorder before each test
   [_recorder clearLog];
 
   [super performTest:aRun];
}

Now to test if the -parserDidStartDocument: event method is sent, I just do this:

- (void)testStartDocument
{
        NSString *string = @"Hello Markdown";
        DTMarkdownParser *parser = [self _parserForString:string options:0];
 
        BOOL result = [parser parse];
        STAssertTrue(result, @"Parser should return YES");
 
        DTAssertInvocationRecorderContainsCallWithParameter(_recorder, @selector(parserDidStartDocument:), nil);
}

You see I created my own assert macro which abstracts away the complexity of going into the recorders invocations and seeing if there is any invocation for this selector. To build that I looked at how the SenTest macros work and rejiggered one to do my bidding.

If you are interested to learn how this works you can look at the DTMarkdownParser unit test code. Suffice it to say that the result is that you end up with a very simple and easy to grasp assertion to parse. You assert that there is a call to a certain selector. If there is none then the unit test fails like you expect to.

This works nicely for simple invocations, but for the more complex ones it makes little sense having to check for the presence of all the individual delegate method calls. Instead I build a helper method that constructs “quick and dirty” HTML from the invocations just like a consumer of the API would. This HTML I can now compare against some HTML I got from a markdown editor app.

Conclusion

If you have anything to do with markdown please have a look at DTMarkdownParser. The first released version bears the version number 0.1.0, is tagged as such on GitHub as well as available as Cocoapod.

DTInvocationRecorder is also available for your inspection on GitHub. It’s only part of the test targets but is universally usable. It contains some neat tricks using the Objective-C runtime to dynamically add protocol methods and record them. There is also a nice category on NSInvocation which allows you to retrieve arguments from invocations as objects.

I worked hard on this over the last few days to get the basics working. The TDD approach helped to get the unit test code coverage to 100% and I would like for it to remain this way. If you know of any scenarios that it doesn’t handle correctly as of yet, please open an issue and post your test case.

Or – preferably – go about improving and enhancing the code to cover these special cases. I would love to receive your pull request.

]]>
https://www.cocoanetics.com/2013/10/dtmarkdownparser-0-1-0/feed/ 7 8791