Permalink
Browse files

Fixed HTML and XML handling within example blocks. Closes #54.

The problem was two fold: template engine is specifically instructed to prevent any HTML escaping (otherwise using custom HTML tags within comment code wouldn't be possible), but this renders example blocks containing XML or HTML useless. To overcome this, example blocks are manually escaped by appledoc. Note that this required updating example blocks unit tests - changing to real settings provider as mock didn't do escaping...

Second: GBTokenizer marked HTML/XML opening comment marker `<!--` as a delimiter and removed it from the string, leaving only the remaining text. This interfered with example blocks processing later on. This was actually a bug as delimiter should only be considered a line containing ONLY delimiter chars (at present the regex used was matching only from start of line)!

Also added few more unit tests for validating example blocks prefixed with spaces are detected.

Increased build number to 525.
  • Loading branch information...
1 parent 559284a commit d7ece461880b272becd70efb6f95b96aabaedef4 @tomaz committed Jan 24, 2011
View
@@ -17,6 +17,6 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
- <string>521</string>
+ <string>525</string>
</dict>
</plist>
@@ -318,6 +318,13 @@
/// @name Application-wide HTML helpers
///---------------------------------------------------------------------------------------
+/** Returns a new string by escaping the given HTML.
+
+ @param string HTML string to escape.
+ @return Returns escaped HTML string.
+ */
+- (NSString *)stringByEscapingHTML:(NSString *)string;
+
/** Returns HTML reference name for the given object.
This should only be used for creating anchors that need to be referenced from other parts of the same HTML file. The method works for top-level objects as well as their members.
@@ -138,7 +138,19 @@ - (void)replaceAllOccurencesOfPlaceholderStringsInSettingsValues {
self.docsetCopyrightMessage = [self stringByReplacingOccurencesOfPlaceholdersInString:self.docsetCopyrightMessage];
}
-#pragma mark HTML references handling
+#pragma mark Common HTML handling
+
+- (NSString *)stringByEscapingHTML:(NSString *)string {
+ // Copied directly from GRMustache's GRMustacheVariableElement.m...
+ NSMutableString *result = [NSMutableString stringWithCapacity:5 + ceilf(string.length * 1.1)];
+ [result appendString:string];
+ [result replaceOccurrencesOfString:@"&" withString:@"&amp;" options:NSLiteralSearch range:NSMakeRange(0, result.length)];
+ [result replaceOccurrencesOfString:@"<" withString:@"&lt;" options:NSLiteralSearch range:NSMakeRange(0, result.length)];
+ [result replaceOccurrencesOfString:@">" withString:@"&gt;" options:NSLiteralSearch range:NSMakeRange(0, result.length)];
+ [result replaceOccurrencesOfString:@"\"" withString:@"&quot;" options:NSLiteralSearch range:NSMakeRange(0, result.length)];
+ [result replaceOccurrencesOfString:@"'" withString:@"&apos;" options:NSLiteralSearch range:NSMakeRange(0, result.length)];
+ return result;
+}
- (NSString *)htmlReferenceNameForObject:(GBModelBase *)object {
NSParameterAssert(object != nil);
@@ -125,7 +125,7 @@ - (NSDictionary *)appledocData {
result = [[NSMutableDictionary alloc] init];
[result setObject:@"appledoc" forKey:@"tool"];
[result setObject:@"2.0" forKey:@"version"];
- [result setObject:@"521" forKey:@"build"];
+ [result setObject:@"525" forKey:@"build"];
[result setObject:@"http://appledoc.gentlebytes.com" forKey:@"homepage"];
}
return result;
View
@@ -50,7 +50,7 @@ - (id)initWithSourceTokenizer:(PKTokenizer *)tokenizer filename:(NSString *)file
if (self) {
self.singleLineCommentRegex = @"(?m-s:\\s*///(.*)$)";
self.multiLineCommentRegex = @"(?s:/\\*\\*(.*)\\*/)";
- self.commentDelimiterRegex = @"^[!@#$%^&*()_=+`~,<.>/?;:'\"-]{3,}";
+ self.commentDelimiterRegex = @"^[!@#$%^&*()_=+`~,<.>/?;:'\"-]{3,}$";
self.tokenIndex = 0;
self.lastCommentBuilder = [NSMutableString string];
self.previousCommentBuilder = [NSMutableString string];
@@ -187,7 +187,6 @@ - (BOOL)consumeComments {
}
// Append string value to current comment and proceed with next token.
- value = [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
[self.lastCommentBuilder appendString:value];
self.tokenIndex++;
}
@@ -229,13 +228,14 @@ - (NSString *)commentValueFromString:(NSString *)value {
}];
}
- // Finally remove common line prefix including all spaces and compose all objects into final comment.
+ // Finally remove common line prefix and a single prefix space (but leave multiple spaces to properly handle space prefixed example blocks!) and compose all objects into final comment.
NSCharacterSet *spacesSet = [NSCharacterSet characterSetWithCharactersInString:@" "];
+ NSString *spacesPrefixRegex = @"^ {2,}";
+ NSString *tabPrefixRegex = @"^\t";
NSMutableString *result = [NSMutableString stringWithCapacity:[value length]];
[comments enumerateObjectsUsingBlock:^(NSString *line, NSUInteger idx, BOOL *stop) {
- if (stripPrefix)
- line = [line stringByReplacingOccurrencesOfRegex:prefixRegex withString:@""];
- line = [line stringByTrimmingCharactersInSet:spacesSet];
+ if (stripPrefix) line = [line stringByReplacingOccurrencesOfRegex:prefixRegex withString:@""];
+ if (![line isMatchedByRegex:spacesPrefixRegex] && ![line isMatchedByRegex:tabPrefixRegex]) line = [line stringByTrimmingCharactersInSet:spacesSet];
[result appendString:line];
if (idx < [comments count] - 1) [result appendString:@"\n"];
}];
@@ -229,14 +229,15 @@ - (BOOL)registerExampleBlockFromLines:(NSArray *)lines {
return YES;
}
GBLogDebug(@" - Found example block '%@' at %@.", [stringValue normalizedDescription], self.sourceFileInfo);
+ NSString *escapedHTML = [self.settings stringByEscapingHTML:stringValue];
// If there isn't paragraph registered yet, create one now, otherwise we'll just add the block to previous paragraph.
[self pushParagraphIfStackIsEmpty];
// Prepare paragraph item. Note that we don't use paragraphs stack as currently we don't process the text for cross refs!
GBParagraphSpecialItem *item = [GBParagraphSpecialItem specialItemWithType:GBSpecialItemTypeExample stringValue:stringValue];
GBCommentParagraph *paragraph = [GBCommentParagraph paragraph];
- [paragraph registerItem:[GBParagraphTextItem paragraphItemWithStringValue:stringValue]];
+ [paragraph registerItem:[GBParagraphTextItem paragraphItemWithStringValue:escapedHTML]];
[item registerParagraph:paragraph];
// Register example block to current paragraph.
@@ -20,7 +20,7 @@ @implementation GBCommentsProcessorExamplesTesting
- (void)testProcessCommentWithStore_shouldAttachExampleToPreviousParagraph {
// setup
- GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry mockSettingsProvider]];
+ GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry realSettingsProvider]];
GBComment *comment = [GBComment commentWithStringValue:@"Paragraph\n\n\tDescription"];
// execute
[processor processComment:comment withStore:[GBTestObjectsRegistry store]];
@@ -35,7 +35,7 @@ - (void)testProcessCommentWithStore_shouldAttachExampleToPreviousParagraph {
- (void)testProcessCommentWithStore_shouldDetectMultipleLinesDescriptions {
// setup
- GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry mockSettingsProvider]];
+ GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry realSettingsProvider]];
GBComment *comment = [GBComment commentWithStringValue:@"Paragraph\n\n\tLine1\n\tLine2"];
// execute
[processor processComment:comment withStore:[GBTestObjectsRegistry store]];
@@ -50,7 +50,7 @@ - (void)testProcessCommentWithStore_shouldDetectMultipleLinesDescriptions {
- (void)testProcessCommentWithStore_shouldRemovePrefixTabs {
// setup
- GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry mockSettingsProvider]];
+ GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry realSettingsProvider]];
GBComment *comment = [GBComment commentWithStringValue:@"Paragraph\n\n\tLine"];
// execute
[processor processComment:comment withStore:[GBTestObjectsRegistry store]];
@@ -65,7 +65,7 @@ - (void)testProcessCommentWithStore_shouldRemovePrefixTabs {
- (void)testProcessCommentWithStore_shouldKeepPrefixTabsAfterFirst {
// setup
- GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry mockSettingsProvider]];
+ GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry realSettingsProvider]];
GBComment *comment = [GBComment commentWithStringValue:@"Paragraph\n\n\t\tLine1\n\t\t\t\tLine2"];
// execute
[processor processComment:comment withStore:[GBTestObjectsRegistry store]];
@@ -80,7 +80,7 @@ - (void)testProcessCommentWithStore_shouldKeepPrefixTabsAfterFirst {
- (void)testProcessCommentWithStore_shouldKeepEmptyLinesIfPrefixedWithTab {
// setup
- GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry mockSettingsProvider]];
+ GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry realSettingsProvider]];
GBComment *comment = [GBComment commentWithStringValue:@"Paragraph\n\n\t\tLine1\n\t\n\tLine3"];
// execute
[processor processComment:comment withStore:[GBTestObjectsRegistry store]];
@@ -95,7 +95,7 @@ - (void)testProcessCommentWithStore_shouldKeepEmptyLinesIfPrefixedWithTab {
- (void)testProcessCommentWithStore_shouldCreateParagraphIfNoneSpecifiedBefore {
// setup
- GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry mockSettingsProvider]];
+ GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry realSettingsProvider]];
GBComment *comment = [GBComment commentWithStringValue:@"\tDescription"];
// execute
[processor processComment:comment withStore:[GBTestObjectsRegistry store]];
@@ -108,11 +108,72 @@ - (void)testProcessCommentWithStore_shouldCreateParagraphIfNoneSpecifiedBefore {
[self assertParagraph:item.specialItemDescription containsItems:[GBParagraphTextItem class], @"Description", nil];
}
+#pragma mark Comments prefixed with spaces
+
+- (void)testProcessCommentWithStore_shouldDetectIfPrefixedWithAtLeastFourSpaces {
+ // setup
+ GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry realSettingsProvider]];
+ GBComment *comment1 = [GBComment commentWithStringValue:@" Description"];
+ GBComment *comment2 = [GBComment commentWithStringValue:@" Description"];
+ // execute
+ [processor processComment:comment1 withStore:[GBTestObjectsRegistry store]];
+ [processor processComment:comment2 withStore:[GBTestObjectsRegistry store]];
+ // verify
+ GBCommentParagraph *paragraph1 = comment1.firstParagraph;
+ [self assertParagraph:paragraph1 containsItems:[GBParagraphSpecialItem class], GBNULL, nil];
+ GBParagraphSpecialItem *item1 = [paragraph1.paragraphItems objectAtIndex:0];
+ assertThatInteger(item1.specialItemType, equalToInteger(GBSpecialItemTypeExample));
+ [self assertParagraph:item1.specialItemDescription containsItems:[GBParagraphTextItem class], @"Description", nil];
+ GBCommentParagraph *paragraph2 = comment2.firstParagraph;
+ [self assertParagraph:paragraph2 containsItems:[GBParagraphTextItem class], @" Description", nil];
+}
+
+- (void)testProcessCommentWithStore_shouldKeepAllWhitespaceAfterInitialFourSpaces {
+ // setup
+ GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry realSettingsProvider]];
+ GBComment *comment = [GBComment commentWithStringValue:@" Description"];
+ // execute
+ [processor processComment:comment withStore:[GBTestObjectsRegistry store]];
+ // verify
+ assertThatInteger([[comment paragraphs] count], equalToInteger(1));
+ GBCommentParagraph *paragraph = comment.firstParagraph;
+ [self assertParagraph:paragraph containsItems:[GBParagraphSpecialItem class], GBNULL, nil];
+ GBParagraphSpecialItem *item = [paragraph.paragraphItems objectAtIndex:0];
+ assertThatInteger(item.specialItemType, equalToInteger(GBSpecialItemTypeExample));
+ [self assertParagraph:item.specialItemDescription containsItems:[GBParagraphTextItem class], @" Description", nil];
+}
+
+#pragma mark Special contents parsing
+
+- (void)testProcessCommentWithStore_shouldEscapeHTMLText {
+ // setup
+ GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry realSettingsProvider]];
+ GBComment *comment = [GBComment commentWithStringValue:@"\t<sometag>\"'&</sometag>"];
+ // execute
+ [processor processComment:comment withStore:[GBTestObjectsRegistry store]];
+ // verify
+ GBCommentParagraph *paragraph = comment.firstParagraph;
+ GBParagraphSpecialItem *item = [paragraph.paragraphItems objectAtIndex:0];
+ [self assertParagraph:item.specialItemDescription containsItems:[GBParagraphTextItem class], @"&lt;sometag&gt;&quot;&apos;&amp;&lt;/sometag&gt;", nil];
+}
+
+- (void)testProcessCommentWithStore_shouldEscapeHTMLComments {
+ // setup
+ GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry realSettingsProvider]];
+ GBComment *comment = [GBComment commentWithStringValue:@"\t<!-- .... ->"];
+ // execute
+ [processor processComment:comment withStore:[GBTestObjectsRegistry store]];
+ // verify
+ GBCommentParagraph *paragraph = comment.firstParagraph;
+ GBParagraphSpecialItem *item = [paragraph.paragraphItems objectAtIndex:0];
+ [self assertParagraph:item.specialItemDescription containsItems:[GBParagraphTextItem class], @"&lt;!-- .... -&gt;", nil];
+}
+
#pragma mark Requirements before/after testing
- (void)testProcessCommentWithStore_requiresEmptyLineAfterPreviousParagraphItem {
// setup
- GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry mockSettingsProvider]];
+ GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry realSettingsProvider]];
GBComment *comment = [GBComment commentWithStringValue:@"Paragraph\n\tLine"];
// execute
[processor processComment:comment withStore:[GBTestObjectsRegistry store]];
@@ -124,7 +185,7 @@ - (void)testProcessCommentWithStore_requiresEmptyLineAfterPreviousParagraphItem
- (void)testProcessCommentWithStore_requiresEmptyLineBeforeNextParagraphItem {
// setup
- GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry mockSettingsProvider]];
+ GBCommentsProcessor *processor = [GBCommentsProcessor processorWithSettingsProvider:[GBTestObjectsRegistry realSettingsProvider]];
GBComment *comment1 = [GBComment commentWithStringValue:@"\tLine1\nLine2"];
GBComment *comment2 = [GBComment commentWithStringValue:@"\tLine1\n\nLine2"];
// execute
@@ -265,13 +265,27 @@ - (void)testConsumeFromToUsingBlock_shouldQuitWithoutConsumingCurrentToken {
#pragma mark Comments parsing testing
-- (void)testLastCommentString_shouldTrimSpacesFromBothEnds {
+- (void)testLastCommentString_shouldTrimSpacesFromBothEndsIfPrefixedWithSignleSpace {
// setup & execute
- GBTokenizer *tokenizer = [GBTokenizer tokenizerWithSource:[PKTokenizer tokenizerWithString:@"/// comment \n ONE"] filename:@"file"];
+ GBTokenizer *tokenizer = [GBTokenizer tokenizerWithSource:[PKTokenizer tokenizerWithString:@"/// comment \n ONE"] filename:@"file"];
// verify
assertThat([tokenizer.lastComment stringValue], is(@"comment"));
}
+- (void)testLastCommentString_shouldNotTrimSpacesIfPrefixedWithMultipleSpaces {
+ // setup & execute
+ GBTokenizer *tokenizer = [GBTokenizer tokenizerWithSource:[PKTokenizer tokenizerWithString:@"/// comment \n ONE"] filename:@"file"];
+ // verify
+ assertThat([tokenizer.lastComment stringValue], is(@" comment "));
+}
+
+- (void)testLastCommentString_shouldNotTrimSpacesIfPrefixedWithTab {
+ // setup & execute
+ GBTokenizer *tokenizer = [GBTokenizer tokenizerWithSource:[PKTokenizer tokenizerWithString:@"///\tcomment \n ONE"] filename:@"file"];
+ // verify
+ assertThat([tokenizer.lastComment stringValue], is(@"\tcomment "));
+}
+
- (void)testLastCommentString_shouldGroupSingleLineComments {
// setup & execute
GBTokenizer *tokenizer = [GBTokenizer tokenizerWithSource:[PKTokenizer tokenizerWithString:@"/// line1\n/// line2\n ONE"] filename:@"file"];

0 comments on commit d7ece46

Please sign in to comment.