Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

283 lines
10KB

  1. //
  2. // MMGenerator.m
  3. // MMMarkdown
  4. //
  5. // Copyright (c) 2012 Matt Diephouse.
  6. //
  7. // Permission is hereby granted, free of charge, to any person obtaining a copy
  8. // of this software and associated documentation files (the "Software"), to deal
  9. // in the Software without restriction, including without limitation the rights
  10. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. // copies of the Software, and to permit persons to whom the Software is
  12. // furnished to do so, subject to the following conditions:
  13. //
  14. // The above copyright notice and this permission notice shall be included in
  15. // all copies or substantial portions of the Software.
  16. //
  17. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. // THE SOFTWARE.
  24. //
  25. #import "MMGenerator.h"
  26. #import "MMDocument.h"
  27. #import "MMElement.h"
  28. // This value is used to estimate the length of the HTML output. The length of the markdown document
  29. // is multplied by it to create an NSMutableString with an initial capacity.
  30. static const Float64 kHTMLDocumentLengthMultiplier = 1.25;
  31. static NSString * __HTMLEscapedString(NSString *aString)
  32. {
  33. NSMutableString *result = [aString mutableCopy];
  34. [result replaceOccurrencesOfString:@"&"
  35. withString:@"&"
  36. options:NSLiteralSearch
  37. range:NSMakeRange(0, result.length)];
  38. [result replaceOccurrencesOfString:@"\""
  39. withString:@"""
  40. options:NSLiteralSearch
  41. range:NSMakeRange(0, result.length)];
  42. return result;
  43. }
  44. static NSString *__obfuscatedEmailAddress(NSString *anAddress)
  45. {
  46. NSMutableString *result = [NSMutableString new];
  47. NSString *(^decimal)(unichar c) = ^(unichar c){ return [NSString stringWithFormat:@"&#%d;", c]; };
  48. NSString *(^hex)(unichar c) = ^(unichar c){ return [NSString stringWithFormat:@"&#x%x;", c]; };
  49. NSString *(^raw)(unichar c) = ^(unichar c){ return [NSString stringWithCharacters:&c length:1]; };
  50. NSArray *encoders = @[ decimal, hex, raw ];
  51. for (NSUInteger idx=0; idx<anAddress.length; idx++)
  52. {
  53. unichar character = [anAddress characterAtIndex:idx];
  54. NSString *(^encoder)(unichar c);
  55. if (character == '@')
  56. {
  57. // Make sure that the @ gets encoded
  58. encoder = [encoders objectAtIndex:arc4random_uniform(2)];
  59. }
  60. else
  61. {
  62. int r = arc4random_uniform(100);
  63. encoder = encoders[(r >= 90) ? 2 : (r >= 45) ? 1 : 0];
  64. }
  65. [result appendString:encoder(character)];
  66. }
  67. return result;
  68. }
  69. static NSString * __HTMLStartTagForElement(MMElement *anElement)
  70. {
  71. switch (anElement.type)
  72. {
  73. case MMElementTypeHeader:
  74. return [NSString stringWithFormat:@"<h%u>", (unsigned int)anElement.level];
  75. case MMElementTypeParagraph:
  76. return @"<p>";
  77. case MMElementTypeBulletedList:
  78. return @"<ul>\n";
  79. case MMElementTypeNumberedList:
  80. return @"<ol>\n";
  81. case MMElementTypeListItem:
  82. return @"<li>";
  83. case MMElementTypeBlockquote:
  84. return @"<blockquote>\n";
  85. case MMElementTypeCodeBlock:
  86. return anElement.language ? [NSString stringWithFormat:@"<pre><code class=\"%@\">", anElement.language] : @"<pre><code>";
  87. case MMElementTypeLineBreak:
  88. return @"<br />";
  89. case MMElementTypeHorizontalRule:
  90. return @"\n<hr />\n";
  91. case MMElementTypeStrikethrough:
  92. return @"<del>";
  93. case MMElementTypeStrong:
  94. return @"<strong>";
  95. case MMElementTypeEm:
  96. return @"<em>";
  97. case MMElementTypeCodeSpan:
  98. return @"<code>";
  99. case MMElementTypeImage:
  100. if (anElement.title != nil)
  101. {
  102. return [NSString stringWithFormat:@"<img src=\"%@\" alt=\"%@\" title=\"%@\" />",
  103. __HTMLEscapedString(anElement.href),
  104. __HTMLEscapedString(anElement.stringValue),
  105. __HTMLEscapedString(anElement.title)];
  106. }
  107. return [NSString stringWithFormat:@"<img src=\"%@\" alt=\"%@\" />",
  108. __HTMLEscapedString(anElement.href),
  109. __HTMLEscapedString(anElement.stringValue)];
  110. case MMElementTypeLink:
  111. if (anElement.title != nil)
  112. {
  113. return [NSString stringWithFormat:@"<a title=\"%@\" href=\"%@\">",
  114. __HTMLEscapedString(anElement.title), __HTMLEscapedString(anElement.href)];
  115. }
  116. return [NSString stringWithFormat:@"<a href=\"%@\">", __HTMLEscapedString(anElement.href)];
  117. case MMElementTypeMailTo:
  118. return [NSString stringWithFormat:@"<a href=\"%@\">%@</a>",
  119. __obfuscatedEmailAddress([NSString stringWithFormat:@"mailto:%@", anElement.href]),
  120. __obfuscatedEmailAddress(anElement.href)];
  121. case MMElementTypeEntity:
  122. return anElement.stringValue;
  123. case MMElementTypeTable:
  124. return @"<table>";
  125. case MMElementTypeTableHeader:
  126. return @"<thead><tr>";
  127. case MMElementTypeTableHeaderCell:
  128. return anElement.alignment == MMTableCellAlignmentCenter ? @"<th align='center'>"
  129. : anElement.alignment == MMTableCellAlignmentLeft ? @"<th align='left'>"
  130. : anElement.alignment == MMTableCellAlignmentRight ? @"<th align='right'>"
  131. : @"<th>";
  132. case MMElementTypeTableRow:
  133. return @"<tr>";
  134. case MMElementTypeTableRowCell:
  135. return anElement.alignment == MMTableCellAlignmentCenter ? @"<td align='center'>"
  136. : anElement.alignment == MMTableCellAlignmentLeft ? @"<td align='left'>"
  137. : anElement.alignment == MMTableCellAlignmentRight ? @"<td align='right'>"
  138. : @"<td>";
  139. default:
  140. return nil;
  141. }
  142. }
  143. static NSString * __HTMLEndTagForElement(MMElement *anElement)
  144. {
  145. switch (anElement.type)
  146. {
  147. case MMElementTypeHeader:
  148. return [NSString stringWithFormat:@"</h%u>\n", (unsigned int)anElement.level];
  149. case MMElementTypeParagraph:
  150. return @"</p>\n";
  151. case MMElementTypeBulletedList:
  152. return @"</ul>\n";
  153. case MMElementTypeNumberedList:
  154. return @"</ol>\n";
  155. case MMElementTypeListItem:
  156. return @"</li>\n";
  157. case MMElementTypeBlockquote:
  158. return @"</blockquote>\n";
  159. case MMElementTypeCodeBlock:
  160. return @"</code></pre>\n";
  161. case MMElementTypeStrikethrough:
  162. return @"</del>";
  163. case MMElementTypeStrong:
  164. return @"</strong>";
  165. case MMElementTypeEm:
  166. return @"</em>";
  167. case MMElementTypeCodeSpan:
  168. return @"</code>";
  169. case MMElementTypeLink:
  170. return @"</a>";
  171. case MMElementTypeTable:
  172. return @"</tbody></table>";
  173. case MMElementTypeTableHeader:
  174. return @"</tr></thead><tbody>";
  175. case MMElementTypeTableHeaderCell:
  176. return @"</th>";
  177. case MMElementTypeTableRow:
  178. return @"</tr>";
  179. case MMElementTypeTableRowCell:
  180. return @"</td>";
  181. default:
  182. return nil;
  183. }
  184. }
  185. @interface MMGenerator ()
  186. - (void) _generateHTMLForElement:(MMElement *)anElement
  187. inDocument:(MMDocument *)aDocument
  188. HTML:(NSMutableString *)theHTML
  189. location:(NSUInteger *)aLocation;
  190. @end
  191. @implementation MMGenerator
  192. #pragma mark - Public Methods
  193. - (NSString *)generateHTML:(MMDocument *)aDocument
  194. {
  195. NSString *markdown = aDocument.markdown;
  196. NSUInteger location = 0;
  197. NSUInteger length = markdown.length;
  198. NSMutableString *HTML = [NSMutableString stringWithCapacity:length * kHTMLDocumentLengthMultiplier];
  199. for (MMElement *element in aDocument.elements)
  200. {
  201. if (element.type == MMElementTypeHTML)
  202. {
  203. [HTML appendString:[aDocument.markdown substringWithRange:element.range]];
  204. }
  205. else
  206. {
  207. [self _generateHTMLForElement:element
  208. inDocument:aDocument
  209. HTML:HTML
  210. location:&location];
  211. }
  212. }
  213. return HTML;
  214. }
  215. #pragma mark - Private Methods
  216. - (void)_generateHTMLForElement:(MMElement *)anElement
  217. inDocument:(MMDocument *)aDocument
  218. HTML:(NSMutableString *)theHTML
  219. location:(NSUInteger *)aLocation
  220. {
  221. NSString *startTag = __HTMLStartTagForElement(anElement);
  222. NSString *endTag = __HTMLEndTagForElement(anElement);
  223. if (startTag)
  224. [theHTML appendString:startTag];
  225. for (MMElement *child in anElement.children)
  226. {
  227. if (child.type == MMElementTypeNone)
  228. {
  229. NSString *markdown = aDocument.markdown;
  230. if (child.range.length == 0)
  231. {
  232. [theHTML appendString:@"\n"];
  233. }
  234. else
  235. {
  236. [theHTML appendString:[markdown substringWithRange:child.range]];
  237. }
  238. }
  239. else if (child.type == MMElementTypeHTML)
  240. {
  241. [theHTML appendString:[aDocument.markdown substringWithRange:child.range]];
  242. }
  243. else
  244. {
  245. [self _generateHTMLForElement:child
  246. inDocument:aDocument
  247. HTML:theHTML
  248. location:aLocation];
  249. }
  250. }
  251. if (endTag)
  252. [theHTML appendString:endTag];
  253. }
  254. @end