├── .gitignore ├── .gitmodules ├── CSS ├── KSCSSWriter.h └── KSCSSWriter.m ├── DOM ├── KSXMLWriterDOMAdaptor.h └── KSXMLWriterDOMAdaptor.m ├── Extras ├── KSSitemapWriter.h └── KSSitemapWriter.m ├── KSHTMLWriter.h ├── KSHTMLWriter.key ├── KSHTMLWriter.m ├── KSHTMLWriter.xcodeproj ├── project.pbxproj └── xcshareddata │ └── xcschemes │ ├── KSHTMLWriter.xcscheme │ └── KSHTMLWriterFramework.xcscheme ├── KSHTMLWriter.xcworkspace └── contents.xcworkspacedata ├── KSHTMLWriterFramework ├── KSHTMLWriterFramework-Info.plist ├── KSHTMLWriterFramework-Prefix.pch ├── KSHTMLWriterFramework.h ├── KSHTMLWriterFramework.m └── en.lproj │ └── InfoPlist.strings ├── KSStringXMLEntityEscaping.h ├── KSStringXMLEntityEscaping.m ├── KSXMLAttributes.h ├── KSXMLAttributes.m ├── KSXMLWriter.h ├── KSXMLWriter.m ├── README.mdown └── Tests ├── Classes ├── KSHTMLWriterNormalSnippetTests.m ├── KSHTMLWriterPrettyPrintSnippetTests.m ├── KSHTMLWriterSnippetTests.h ├── KSHTMLWriterSnippetTests.m ├── KSHTMLWriterTests.m ├── KSXMLWriterCompoundTests.m └── KSXMLWriterTests.m ├── Info.plist ├── KSHTMLWriterFramework_Tests.m ├── README.mdown ├── Resources ├── KSHTMLWriterTests.plist ├── KSXMLWriterCompoundTests.plist └── Stub.html ├── Scripts └── test.sh ├── Stringification.testdata └── StringificationTests.m /.gitignore: -------------------------------------------------------------------------------- 1 | # Mac OS X 2 | *.DS_Store 3 | 4 | # Xcode 5 | *.pbxuser 6 | *.mode1v3 7 | *.mode2v3 8 | *.perspectivev3 9 | *.xcuserstate 10 | project.xcworkspace/ 11 | xcuserdata/ 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "KSWriter"] 2 | path = KSWriter 3 | url = https://github.com/karelia/KSWriter.git 4 | [submodule "Tests/ECUnitTests"] 5 | path = Tests/ECUnitTests 6 | url = https://github.com/karelia/ECUnitTests.git 7 | -------------------------------------------------------------------------------- /CSS/KSCSSWriter.h: -------------------------------------------------------------------------------- 1 | // 2 | // KSCSSWriter.h 3 | // 4 | // Created by Mike Abdullah 5 | // Copyright © 2010 Karelia Software 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 | 26 | #import 27 | 28 | 29 | @interface KSCSSWriter : NSObject 30 | { 31 | @private 32 | KSWriter *_output; 33 | } 34 | 35 | - initWithOutputWriter:(KSWriter *)output; 36 | 37 | // Writes the string followed enough newlines to carry on writing 38 | - (void)writeCSSString:(NSString *)cssString; 39 | 40 | - (void)writeIDSelector:(NSString *)ID; 41 | - (void)writeDeclarationBlock:(NSString *)declarations; 42 | 43 | @end 44 | -------------------------------------------------------------------------------- /CSS/KSCSSWriter.m: -------------------------------------------------------------------------------- 1 | // 2 | // KSCSSWriter.m 3 | // 4 | // Created by Mike Abdullah 5 | // Copyright © 2010 Karelia Software 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 | 26 | #import "KSCSSWriter.h" 27 | 28 | 29 | @implementation KSCSSWriter 30 | 31 | - initWithOutputWriter:(KSWriter *)output; 32 | { 33 | if (self = [self init]) 34 | { 35 | _output = [output retain]; 36 | } 37 | return self; 38 | } 39 | 40 | - (void)dealloc 41 | { 42 | [_output release]; 43 | [super dealloc]; 44 | } 45 | 46 | - (void)writeCSSString:(NSString *)cssString; 47 | { 48 | [self writeString:cssString]; 49 | if (![cssString hasSuffix:@"\n"]) [self writeString:@"\n"]; 50 | [self writeString:@"\n"]; 51 | } 52 | 53 | - (void)writeIDSelector:(NSString *)ID; 54 | { 55 | [self writeString:@"#"]; 56 | [self writeString:ID]; 57 | } 58 | 59 | - (void)writeDeclarationBlock:(NSString *)declarations; 60 | { 61 | [self writeString:@" {"]; 62 | [self writeString:declarations]; 63 | [self writeString:@"}"]; 64 | 65 | // Could be smarter and analyze declarations for newlines 66 | } 67 | 68 | - (void)writeString:(NSString *)string; { [_output writeString:string]; } 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /DOM/KSXMLWriterDOMAdaptor.h: -------------------------------------------------------------------------------- 1 | // 2 | // KSXMLWriterDOMAdaptor.h 3 | // Sandvox 4 | // 5 | // Created by Mike Abdullah on 25/06/2010. 6 | // Copyright © 2010 Karelia Software 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | #import 28 | #import "KSXMLWriter.h" 29 | 30 | 31 | typedef NS_OPTIONS(NSInteger, KSXMLWriterDOMAdaptorOptions) { 32 | KSXMLWriterOptionsNone = NSXMLNodeOptionsNone, 33 | KSXMLWriterDOMAdaptorPrettyPrint = NSXMLNodePrettyPrint, 34 | KSXMLWriterDOMAdaptorNormalize = 1 << 31, 35 | }; 36 | 37 | 38 | @protocol KSXMLWriterDOMAdaptorDelegate; 39 | @interface KSXMLWriterDOMAdaptor : NSObject 40 | { 41 | @private 42 | KSXMLWriter *_writer; 43 | KSXMLWriterDOMAdaptorOptions _options; 44 | 45 | id _delegate; 46 | } 47 | 48 | 49 | #pragma mark Convenience 50 | + (NSString *)outerHTMLOfDOMElement:(DOMElement *)element; 51 | + (NSString *)outerXMLOfDOMElement:(DOMElement *)element options:(KSXMLWriterDOMAdaptorOptions)options; 52 | 53 | 54 | #pragma mark Init 55 | - (id)initWithXMLWriter:(KSXMLWriter *)writer; // no options 56 | - (id)initWithXMLWriter:(KSXMLWriter *)writer options:(KSXMLWriterDOMAdaptorOptions)options; 57 | 58 | @property(nonatomic, retain, readonly) KSXMLWriter *XMLWriter; 59 | @property(nonatomic, readonly) KSXMLWriterDOMAdaptorOptions options; 60 | 61 | 62 | #pragma mark High Level 63 | - (void)writeDOMElement:(DOMElement *)element; // like -outerHTML 64 | - (void)writeInnerOfDOMNode:(DOMNode *)node; // like -innerHTML 65 | - (void)writeDOMRange:(DOMRange *)range; 66 | 67 | 68 | #pragma mark Implementation 69 | - (void)writeInnerOfDOMNode:(DOMNode *)node startAtChild:(DOMNode *)aNode; 70 | - (void)startElement:(NSString *)elementName withDOMElement:(DOMElement *)element; // open the tag and write attributes 71 | - (DOMNode *)endElementWithDOMElement:(DOMElement *)element; // returns the next sibling to write 72 | - (DOMNode *)writeComment:(NSString *)comment withDOMComment:(DOMComment *)commentNode; 73 | 74 | 75 | #pragma mark Pseudo-delegate 76 | 77 | // Default implementation returns element. To customise writing, subclass method to do its own writing and return the node to write instead (generally the element's next sibling) 78 | - (DOMNode *)willWriteDOMElement:(DOMElement *)element; 79 | 80 | - (DOMNode *)willWriteDOMText:(DOMText *)text; 81 | - (DOMNode *)didWriteDOMText:(DOMText *)text nextNode:(DOMNode *)nextNode; // for any post-processing 82 | 83 | 84 | #pragma mark Delegate 85 | @property(nonatomic, assign) id delegate; 86 | 87 | @end 88 | 89 | 90 | #pragma mark - 91 | 92 | 93 | @protocol KSXMLWriterDOMAdaptorDelegate 94 | 95 | /** 96 | Gives an opportunity to customise how the element is handled. Return `element` untouched for the 97 | adaptor to do its usual work. Otherwise, return a different node to move onto that. You might well 98 | perform your stringification logic for the element before returning. 99 | */ 100 | - (DOMNode *)DOMAdaptor:(KSXMLWriterDOMAdaptor *)writer willWriteDOMElement:(DOMElement *)element; 101 | 102 | @end 103 | -------------------------------------------------------------------------------- /DOM/KSXMLWriterDOMAdaptor.m: -------------------------------------------------------------------------------- 1 | // 2 | // KSXMLWriterDOMAdaptor.m 3 | // Sandvox 4 | // 5 | // Created by Mike Abdullah on 25/06/2010. 6 | // Copyright © 2010 Karelia Software 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | #import "KSXMLWriterDOMAdaptor.h" 28 | 29 | #import "KSHTMLWriter.h" 30 | 31 | 32 | @interface DOMNode (KSDOMToHTMLWriter) 33 | 34 | // All nodes can be written. We just don't really want to expose this implementation detail. DOMElement uses it to recurse down through element contents. 35 | - (DOMNode *)ks_writeHTML:(KSXMLWriterDOMAdaptor *)writer; 36 | - (DOMNode *)ks_writeHTML:(KSXMLWriterDOMAdaptor *)writer fromRange:(DOMRange *)range; 37 | 38 | - (void)ks_writeContent:(KSXMLWriterDOMAdaptor *)writer fromRange:(DOMRange *)range; 39 | 40 | - (BOOL)ks_isDescendantOfDOMNode:(DOMNode *)possibleAncestor; 41 | 42 | @end 43 | 44 | 45 | #pragma mark - 46 | 47 | 48 | @implementation KSXMLWriterDOMAdaptor 49 | 50 | - (id)initWithXMLWriter:(KSXMLWriter *)writer; 51 | { 52 | return [self initWithXMLWriter:writer options:NSXMLNodeOptionsNone]; 53 | } 54 | 55 | - (id)initWithXMLWriter:(KSXMLWriter *)writer options:(KSXMLWriterDOMAdaptorOptions)options; 56 | { 57 | if (self = [self init]) 58 | { 59 | _writer = [writer retain]; 60 | _options = options; 61 | } 62 | return self; 63 | } 64 | 65 | - (void) dealloc; 66 | { 67 | [_writer release]; 68 | [super dealloc]; 69 | } 70 | 71 | @synthesize XMLWriter = _writer; 72 | @synthesize options = _options; 73 | 74 | #pragma mark Convenience 75 | 76 | + (NSString *)outerHTMLOfDOMElement:(DOMElement *)element; 77 | { 78 | KSWriter *output = [KSWriter stringWriterWithEncoding:NSUnicodeStringEncoding]; 79 | KSHTMLWriter *htmlWriter = [[KSHTMLWriter alloc] initWithOutputWriter:output]; 80 | KSXMLWriterDOMAdaptor *adaptor = [[self alloc] initWithXMLWriter:htmlWriter]; 81 | 82 | [adaptor writeDOMElement:element]; 83 | 84 | [adaptor release]; 85 | [htmlWriter release]; 86 | 87 | return output.string; 88 | } 89 | 90 | + (NSString *)outerXMLOfDOMElement:(DOMElement *)element options:(KSXMLWriterDOMAdaptorOptions)options; 91 | { 92 | KSWriter *output = [KSWriter stringWriterWithEncoding:NSUnicodeStringEncoding]; 93 | KSXMLWriter *xmlWriter = [[KSXMLWriter alloc] initWithOutputWriter:output]; 94 | KSXMLWriterDOMAdaptor *adaptor = [[self alloc] initWithXMLWriter:xmlWriter options:options]; 95 | 96 | [adaptor writeDOMElement:element]; 97 | 98 | [adaptor release]; 99 | [xmlWriter release]; 100 | 101 | return output.string; 102 | } 103 | 104 | #pragma mark High Level 105 | 106 | - (void)writeDOMElement:(DOMElement *)element; // like -outerHTML 107 | { 108 | [self startElement:[[element tagName] lowercaseString] withDOMElement:element]; 109 | [self writeInnerOfDOMNode:element]; 110 | [self endElementWithDOMElement:element]; 111 | } 112 | 113 | - (void)writeInnerOfDOMNode:(DOMNode *)element; // like -innerHTML 114 | { 115 | [self writeInnerOfDOMNode:element startAtChild:[element firstChild]]; 116 | } 117 | 118 | - (void)writeDOMRange:(DOMRange *)range; 119 | { 120 | DOMNode *ancestor = [range commonAncestorContainer]; 121 | [ancestor ks_writeContent:self fromRange:range]; 122 | } 123 | 124 | #pragma mark Implementation 125 | 126 | - (void)writeInnerOfDOMNode:(DOMNode *)element startAtChild:(DOMNode *)aNode; 127 | { 128 | // It's best to iterate using a Linked List-like approach in case the iteration also modifies the DOM 129 | while (aNode) 130 | { 131 | aNode = [aNode ks_writeHTML:self]; 132 | } 133 | } 134 | 135 | - (void)startElement:(NSString *)elementName withDOMElement:(DOMElement *)element; // open the tag and write attributes 136 | { 137 | // Write attributes 138 | if ([element hasAttributes]) // -[DOMElement attributes] is slow as it has to allocate an object. #78691 139 | { 140 | DOMNamedNodeMap *attributes = [element attributes]; 141 | unsigned index; 142 | for (index = 0; index < [attributes length]; index++) 143 | { 144 | DOMAttr *anAttribute = (DOMAttr *)[attributes item:index]; 145 | [[self XMLWriter] pushAttribute:[anAttribute name] value:[anAttribute value]]; 146 | } 147 | } 148 | 149 | 150 | // We leave the writer to do pretty-printing 151 | [self.XMLWriter startElement:elementName]; 152 | } 153 | 154 | - (DOMNode *)endElementWithDOMElement:(DOMElement *)element; // returns the next sibling to write 155 | { 156 | [[self XMLWriter] endElement]; 157 | return [element nextSibling]; 158 | } 159 | 160 | - (DOMNode *)writeComment:(NSString *)comment withDOMComment:(DOMComment *)commentNode; 161 | { 162 | [[self XMLWriter] writeComment:comment]; 163 | return [commentNode nextSibling]; 164 | } 165 | 166 | #pragma mark Pseudo-delegate 167 | 168 | - (DOMNode *)willWriteDOMText:(DOMText *)text; { return text; } 169 | 170 | - (DOMNode *)didWriteDOMText:(DOMText *)textNode nextNode:(DOMNode *)nextNode; 171 | { 172 | // Is the next node also text? If so, normalize by appending to textNode. 173 | if ([self options] & KSXMLWriterDOMAdaptorNormalize) 174 | { 175 | if ([nextNode nodeType] == DOM_TEXT_NODE) 176 | { 177 | // Do usual writing. Produces correct output, and handles possibility of a chain of unnormalized text nodes 178 | DOMNode *nodeToAppend = nextNode; 179 | nextNode = [nodeToAppend ks_writeHTML:self]; 180 | 181 | 182 | // Maintain selection 183 | /*WebView *webView = [[[textNode ownerDocument] webFrame] webView]; 184 | DOMRange *selection = [webView selectedDOMRange]; 185 | NSSelectionAffinity affinity = [webView selectionAffinity]; 186 | 187 | NSUInteger length = [textNode length]; 188 | NSIndexPath *startPath = [[selection ks_startIndexPathFromNode:nodeToAppend] indexPathByAddingToLastIndex:length]; 189 | 190 | NSIndexPath *endPath = [[selection ks_endIndexPathFromNode:nodeToAppend] indexPathByAddingToLastIndex:length]; 191 | if (!endPath) 192 | { 193 | // When selection is at end of textNode, WebKit extends selection to cover all of appended text. #136170 194 | endPath = [selection ks_endIndexPathFromNode:textNode]; 195 | }*/ 196 | 197 | 198 | // Delete node by appending to ourself 199 | [textNode appendData:[nodeToAppend nodeValue]]; 200 | [[nodeToAppend parentNode] removeChild:nodeToAppend]; 201 | 202 | 203 | // Restore selection 204 | /*if (startPath) [selection ks_setStartWithIndexPath:startPath fromNode:textNode]; 205 | if (endPath) [selection ks_setEndWithIndexPath:endPath fromNode:textNode]; 206 | if (startPath || endPath) [webView setSelectedDOMRange:selection affinity:affinity];*/ 207 | } 208 | } 209 | 210 | return nextNode; 211 | } 212 | 213 | - (DOMNode *)willWriteDOMElement:(DOMElement *)element 214 | { 215 | if ([self delegate]) 216 | { 217 | return [[self delegate] DOMAdaptor:self willWriteDOMElement:element]; 218 | } 219 | else 220 | { 221 | return element; 222 | } 223 | } 224 | 225 | #pragma mark Delegate 226 | 227 | @synthesize delegate = _delegate; 228 | 229 | @end 230 | 231 | 232 | #pragma mark - 233 | 234 | 235 | @implementation DOMNode (KSDOMToHTMLWriter) 236 | 237 | - (DOMNode *)ks_writeHTML:(KSXMLWriterDOMAdaptor *)writer; 238 | { 239 | // Recurse through child nodes 240 | DOMNode *aNode = [self firstChild]; 241 | while (aNode) 242 | { 243 | aNode = [aNode ks_writeHTML:writer]; 244 | } 245 | 246 | return [self nextSibling]; 247 | } 248 | 249 | - (DOMNode *)ks_writeHTML:(KSXMLWriterDOMAdaptor *)writer fromRange:(DOMRange *)range; 250 | { 251 | [self ks_writeContent:writer fromRange:range]; 252 | return [self nextSibling]; 253 | } 254 | 255 | - (void)ks_writeContent:(KSXMLWriterDOMAdaptor *)writer fromRange:(DOMRange *)range; 256 | { 257 | // If we begin outside the range, figure out the first child that actually belongs in the range 258 | DOMNode *aNode = [self firstChild]; 259 | 260 | DOMNode *startContainer = [range startContainer]; 261 | if (self == startContainer) 262 | { 263 | aNode = [[self childNodes] item:[range startOffset]]; 264 | } 265 | else if ([startContainer ks_isDescendantOfDOMNode:self]) 266 | { 267 | while (aNode) 268 | { 269 | if ([[range startContainer] ks_isDescendantOfDOMNode:aNode]) break; 270 | aNode = [aNode nextSibling]; 271 | } 272 | } 273 | 274 | 275 | // Write child nodes that fall within the range 276 | DOMNode *endContainer = [range endContainer]; 277 | DOMNode *endNode = (self == endContainer) ? [[self childNodes] item:([range endOffset] - 1)] : nil; 278 | 279 | while (aNode) 280 | { 281 | DOMNode *nextNode = [aNode ks_writeHTML:writer fromRange:range]; 282 | 283 | if (aNode == endNode || [endContainer ks_isDescendantOfDOMNode:aNode]) 284 | { 285 | break; 286 | } 287 | 288 | aNode = nextNode; 289 | } 290 | } 291 | 292 | - (BOOL)ks_isDescendantOfDOMNode:(DOMNode *)possibleAncestor; 293 | { 294 | DOMNode *aNode = self; 295 | while (aNode) 296 | { 297 | if (aNode == possibleAncestor) return YES; 298 | aNode = [aNode parentNode]; 299 | } 300 | 301 | return NO; 302 | } 303 | 304 | @end 305 | 306 | 307 | #pragma mark - 308 | 309 | 310 | @implementation DOMElement (KSDOMToHTMLWriter) 311 | 312 | - (DOMNode *)ks_writeHTML:(KSXMLWriterDOMAdaptor *)adaptor; 313 | { 314 | // *Elements* are where the clever recursion starts, so switch responsibility back to the writer. 315 | DOMNode *node = [adaptor willWriteDOMElement:self]; 316 | if (node == self) 317 | { 318 | [adaptor startElement:[[self tagName] lowercaseString] withDOMElement:self]; 319 | [adaptor writeInnerOfDOMNode:self]; 320 | return [adaptor endElementWithDOMElement:self]; 321 | } 322 | else 323 | { 324 | return node; 325 | } 326 | } 327 | 328 | - (DOMNode *)ks_writeHTML:(KSXMLWriterDOMAdaptor *)adaptor fromRange:(DOMRange *)range; 329 | { 330 | // Bit of a special case. When a DOM range ends at the start of an element 331 | if ([range endContainer] == self && [range endOffset] == 0) 332 | { 333 | [(KSHTMLWriter *)[adaptor XMLWriter] writeLineBreak]; 334 | return nil; 335 | } 336 | 337 | 338 | // Let pseudo-delegate jump in if it likes 339 | DOMNode *node = [adaptor willWriteDOMElement:self]; 340 | if (node != self) 341 | { 342 | return node; 343 | } 344 | 345 | 346 | // Start the element 347 | [adaptor startElement:[[self tagName] lowercaseString] withDOMElement:self]; 348 | 349 | // Child nodes 350 | DOMNode *result = [super ks_writeHTML:adaptor fromRange:range]; 351 | 352 | // Close the tag 353 | [[adaptor XMLWriter] endElement]; 354 | 355 | return result; 356 | } 357 | /* 358 | - (void)writeCleanedHTMLToContext:(KSDOMToHTMLWriter *)writer innards:(BOOL)writeInnards; 359 | { 360 | [writer startElementWithDOMElement:self]; 361 | 362 | 363 | if (!sTagsThatCanBeSelfClosed) 364 | { 365 | sTagsThatCanBeSelfClosed = [[NSSet alloc] initWithObjects:@"img", @"br", @"hr", @"p", @"meta", @"link", @"base", @"param", @"source", nil]; 366 | } 367 | 368 | 369 | NSString *tagName = [[self tagName] lowercaseString]; 370 | 371 | if ([self hasChildNodes] || ![sTagsThatCanBeSelfClosed containsObject:tagName]) 372 | { 373 | [writer closeStartTag]; // close the node first 374 | 375 | if (nil == sTagsWithNewlineOnOpen) 376 | { 377 | sTagsWithNewlineOnOpen = [[NSSet alloc] initWithObjects:@"head", @"body", @"ul", @"ol", @"table", @"tr", nil]; 378 | } 379 | if (writeInnards) 380 | { 381 | if ([self hasChildNodes]) 382 | { 383 | [self writeCleanedInnerHTMLToContext:writer]; // <----- RECURSION POINT 384 | } 385 | [writer endElement]; 386 | } 387 | } 388 | else // no children, self-close tag. 389 | { 390 | [writer closeEmptyElementTag]; 391 | } 392 | 393 | if (writeInnards) // only deal with newline if we're doing the innards too 394 | { 395 | if (!sTagsWithNewlineOnClose) 396 | { 397 | sTagsWithNewlineOnClose = [[NSSet alloc] initWithObjects:@"ul", @"ol", @"table", @"li", @"p", @"h1", @"h2", @"h3", @"h4", @"blockquote", @"br", @"pre", @"td", @"tr", @"div", @"hr", nil]; 398 | } 399 | } 400 | } 401 | */ 402 | @end 403 | 404 | 405 | #pragma mark - 406 | 407 | 408 | @implementation DOMCharacterData (KSDOMToHTMLWriter) 409 | 410 | - (DOMNode *)writeData:(NSString *)data toHTMLWriter:(KSXMLWriterDOMAdaptor *)adaptor; 411 | { 412 | /* The text to write is passed in (rather than calling [self data]) so as to handle writing a subset of it 413 | */ 414 | 415 | 416 | if ([adaptor options] & KSXMLWriterDOMAdaptorPrettyPrint) 417 | { 418 | // Unecessary whitespace should be trimmed here 419 | // For text inside HTML elements like , whitespace has meaning, so domn't trim it 420 | KSXMLWriter *writer = [adaptor XMLWriter]; 421 | NSString *parentElement = [writer topElement]; 422 | if (!parentElement || ![writer.class shouldPrettyPrintElementInline:parentElement]) 423 | { 424 | static NSCharacterSet *nonWhitespace; 425 | if (!nonWhitespace) nonWhitespace = [[[NSCharacterSet whitespaceAndNewlineCharacterSet] invertedSet] copy]; 426 | 427 | 428 | BOOL isFirst = [self previousSibling] == nil; 429 | BOOL isLast = [self nextSibling] == nil; 430 | 431 | if (isFirst) 432 | { 433 | if (isLast) 434 | { 435 | data = [data stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; 436 | } 437 | else 438 | { 439 | // Trim off starting whitespace; ignore complete whitespace 440 | NSUInteger nonWhitespaceStart = [data rangeOfCharacterFromSet:nonWhitespace].location; 441 | if (nonWhitespaceStart == NSNotFound) return [super ks_writeHTML:adaptor]; 442 | if (nonWhitespaceStart > 0) data = [data substringFromIndex:nonWhitespaceStart]; 443 | 444 | // Trailing whitespace should be a single space character; not a newline or similar 445 | NSRange nonWhitespaceEnd = [data rangeOfCharacterFromSet:nonWhitespace options:NSBackwardsSearch]; 446 | if (NSMaxRange(nonWhitespaceEnd) < [data length]) 447 | { 448 | NSUInteger length = [data length]; 449 | NSUInteger whitespaceLength = length - NSMaxRange(nonWhitespaceEnd); 450 | 451 | if (whitespaceLength > 1 || [data characterAtIndex:length - 1] != ' ') 452 | { 453 | data = [data stringByReplacingCharactersInRange:NSMakeRange(NSMaxRange(nonWhitespaceEnd), whitespaceLength) 454 | withString:@" "]; 455 | } 456 | } 457 | } 458 | } 459 | else if (isLast) 460 | { 461 | // Trim off ending whitespace; ignore complete whitespace 462 | NSRange nonWhitespaceEnd = [data rangeOfCharacterFromSet:nonWhitespace options:NSBackwardsSearch]; 463 | if (nonWhitespaceEnd.location == NSNotFound) return [super ks_writeHTML:adaptor]; 464 | 465 | nonWhitespaceEnd.location++; 466 | if (nonWhitespaceEnd.location < [data length]) data = [data substringToIndex:nonWhitespaceEnd.location]; 467 | } 468 | else 469 | { 470 | // Ignore complete whitespace, but let all else through 471 | NSRange nonWhitespaceStart = [data rangeOfCharacterFromSet:nonWhitespace options:0]; 472 | if (nonWhitespaceStart.location == NSNotFound) return [super ks_writeHTML:adaptor]; 473 | } 474 | 475 | // Ignore nodes which are naught but whitespace 476 | if ([data length] == 0) return [super ks_writeHTML:adaptor]; 477 | } 478 | } 479 | 480 | 481 | [[adaptor XMLWriter] writeCharacters:data]; 482 | return [super ks_writeHTML:adaptor]; 483 | } 484 | 485 | - (DOMNode *)ks_writeHTML:(KSXMLWriterDOMAdaptor *)writer; 486 | { 487 | DOMNode *result = [self writeData:[self data] toHTMLWriter:writer]; 488 | return result; 489 | } 490 | 491 | - (void)ks_writeContent:(KSXMLWriterDOMAdaptor *)writer fromRange:(DOMRange *)range; 492 | { 493 | // Character data treats that text as its content. This is so you can specify a substring using the offsets in DOMRange 494 | NSString *text = [self data]; 495 | 496 | if ([range endContainer] == self) 497 | { 498 | text = [text substringToIndex:[range endOffset]]; 499 | } 500 | if ([range startContainer] == self) 501 | { 502 | text = [text substringFromIndex:[range startOffset]]; 503 | } 504 | 505 | [self writeData:text toHTMLWriter:writer]; 506 | } 507 | 508 | @end 509 | 510 | 511 | @implementation DOMComment (KSDOMToHTMLWriter) 512 | 513 | - (DOMNode *)writeData:(NSString *)data toHTMLWriter:(KSXMLWriterDOMAdaptor *)adaptor; 514 | { 515 | return [adaptor writeComment:data withDOMComment:self]; 516 | } 517 | 518 | @end 519 | 520 | 521 | @implementation DOMText (KSDOMToHTMLWriter) 522 | 523 | - (DOMNode *)ks_writeHTML:(KSXMLWriterDOMAdaptor *)adaptor; 524 | { 525 | DOMNode *result = [adaptor willWriteDOMText:self]; 526 | if (result != self) return result; 527 | 528 | result = [super ks_writeHTML:adaptor]; 529 | result = [adaptor didWriteDOMText:self nextNode:result]; 530 | 531 | return result; 532 | } 533 | 534 | @end 535 | 536 | 537 | 538 | @implementation DOMCDATASection (KSDOMToHTMLWriter) 539 | 540 | - (DOMNode *)writeData:(NSString *)data toHTMLWriter:(KSXMLWriterDOMAdaptor *)adaptor; 541 | { 542 | [[adaptor XMLWriter] writeString:[NSString stringWithFormat:@"", data]]; 543 | return [self nextSibling]; 544 | } 545 | 546 | @end 547 | -------------------------------------------------------------------------------- /Extras/KSSitemapWriter.h: -------------------------------------------------------------------------------- 1 | // 2 | // KSSitemapWriter.h 3 | // Sandvox 4 | // 5 | // Created by Mike Abdullah on 10/03/2012. 6 | // Copyright © 2012 Karelia Software 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | #import 28 | 29 | 30 | extern NSString * const KSSitemapChangeMapFrequencyAlways; 31 | extern NSString * const KSSitemapChangeMapFrequencyHourly; 32 | extern NSString * const KSSitemapChangeMapFrequencyDaily; 33 | extern NSString * const KSSitemapChangeMapFrequencyWeekly; 34 | extern NSString * const KSSitemapChangeMapFrequencyMonthly; 35 | extern NSString * const KSSitemapChangeMapFrequencyYearly; 36 | extern NSString * const KSSitemapChangeMapFrequencyNever; 37 | 38 | extern NSUInteger const KSSitemapMaxURLLength; 39 | extern NSUInteger const KSSitemapMaxNumberOfURLs; 40 | 41 | // The official spec is 10MB which we declare here. However, Google say they accept up to 50MB 42 | extern NSUInteger const KSSitemapMaxFileSize; 43 | 44 | 45 | @interface KSSitemapWriter : NSObject 46 | { 47 | @private 48 | KSXMLWriter *_writer; 49 | } 50 | 51 | - (id)initWithOutputWriter:(KSWriter *)output; 52 | 53 | // URL is compulsary; all else optional 54 | - (void)writeURL:(NSURL *)loc // should be sub-path of the folder containing the sitemap. avoid exceeding KSSitemapMaxURLLength 55 | modificationDate:(NSDate *)lastMod 56 | changeFrequency:(NSString *)changeFreq 57 | priority:(NSNumber *)priority; // between 0 and 1. If nil, search engines assume 0.5 58 | 59 | // Call when done writing URLs to properly end the XML document 60 | - (void)close; 61 | 62 | @end 63 | 64 | 65 | #pragma mark - 66 | 67 | 68 | extern NSUInteger const KSSitemapIndexMaxNumberOfSitemaps; // the FAQ claims 1,000 as the limit, but the protocol spec and everything else disagrees 69 | extern NSUInteger const KSSitemapIndexMaxFileSize; // 10MB 70 | 71 | 72 | @interface KSSitemapIndexWriter : NSObject 73 | { 74 | @private 75 | KSXMLWriter *_writer; 76 | } 77 | 78 | - (id)initWithOutputWriter:(KSWriter *)output; 79 | 80 | // URL is compulsary; all else optional 81 | - (void)writeSitemapWithLocation:(NSURL *)loc modificationDate:(NSDate *)lastMod; 82 | 83 | // Call when done writing sitemaps to properly end the XML document 84 | - (void)close; 85 | 86 | @end -------------------------------------------------------------------------------- /Extras/KSSitemapWriter.m: -------------------------------------------------------------------------------- 1 | // 2 | // KSSitemapWriter.m 3 | // Sandvox 4 | // 5 | // Created by Mike Abdullah on 10/03/2012. 6 | // Copyright © 2012 Karelia Software 7 | // 8 | // Permission is hereby granted, free of charge, to any person obtaining a copy 9 | // of this software and associated documentation files (the "Software"), to deal 10 | // in the Software without restriction, including without limitation the rights 11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | // copies of the Software, and to permit persons to whom the Software is 13 | // furnished to do so, subject to the following conditions: 14 | // 15 | // The above copyright notice and this permission notice shall be included in 16 | // all copies or substantial portions of the Software. 17 | // 18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | // THE SOFTWARE. 25 | // 26 | 27 | #import "KSSitemapWriter.h" 28 | 29 | 30 | NSString * const KSSitemapChangeMapFrequencyAlways = @"always"; 31 | NSString * const KSSitemapChangeMapFrequencyHourly = @"hourly"; 32 | NSString * const KSSitemapChangeMapFrequencyDaily = @"daily"; 33 | NSString * const KSSitemapChangeMapFrequencyWeekly = @"weekly"; 34 | NSString * const KSSitemapChangeMapFrequencyMonthly = @"monthly"; 35 | NSString * const KSSitemapChangeMapFrequencyYearly = @"yearly"; 36 | NSString * const KSSitemapChangeMapFrequencyNever = @"never"; 37 | 38 | NSUInteger const KSSitemapMaxURLLength = 2048; 39 | NSUInteger const KSSitemapMaxNumberOfURLs = 50000; 40 | NSUInteger const KSSitemapMaxFileSize = 10485760; 41 | 42 | NSUInteger const KSSitemapIndexMaxNumberOfSitemaps = 50000; 43 | NSUInteger const KSSitemapIndexMaxFileSize = 10485760; 44 | 45 | 46 | @implementation KSSitemapWriter 47 | 48 | - (id)initWithOutputWriter:(KSWriter *)output; 49 | { 50 | OBPRECONDITION(output.encoding == NSUTF8StringEncoding); 51 | 52 | if (self = [self init]) 53 | { 54 | _writer = [[KSXMLWriter alloc] initWithOutputWriter:output]; 55 | [_writer writeString:@"\n"]; 56 | 57 | [_writer pushAttribute:@"xmlns" value:@"http://www.sitemaps.org/schemas/sitemap/0.9"]; 58 | [_writer startElement:@"urlset"]; 59 | } 60 | 61 | return self; 62 | } 63 | 64 | - (void)writeURL:(NSURL *)loc modificationDate:(NSDate *)lastMod changeFrequency:(NSString *)changeFreq priority:(NSNumber *)priority; 65 | { 66 | [_writer writeElement:@"url" content:^{ 67 | [_writer writeElement:@"loc" text:[loc absoluteString]]; 68 | 69 | if (lastMod) 70 | { 71 | #pragma clang diagnostic push 72 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 73 | NSString *lastModText = [lastMod descriptionWithCalendarFormat:@"%Y-%m-%dT%H:%M:%SZ" 74 | timeZone:[NSTimeZone timeZoneWithAbbreviation:@"GMT"] 75 | locale:nil]; 76 | #pragma clang diagnostic pop 77 | [_writer writeElement:@"lastmod" text:lastModText]; 78 | } 79 | 80 | if (changeFreq) [_writer writeElement:@"changefreq" text:changeFreq]; 81 | if (priority) [_writer writeElement:@"priority" text:[NSString stringWithFormat:@"%.02f", [priority floatValue]]]; 82 | }]; 83 | } 84 | 85 | - (void)close; 86 | { 87 | [_writer endElement]; // 88 | [_writer release]; _writer = nil; 89 | } 90 | 91 | - (void)dealloc 92 | { 93 | [self close]; // releases _writer 94 | [super dealloc]; 95 | } 96 | 97 | @end 98 | 99 | 100 | #pragma mark - 101 | 102 | 103 | @implementation KSSitemapIndexWriter 104 | 105 | - (id)initWithOutputWriter:(KSWriter *)output; 106 | { 107 | OBPRECONDITION(output.encoding == NSUTF8StringEncoding); 108 | 109 | if (self = [self init]) 110 | { 111 | _writer = [[KSXMLWriter alloc] initWithOutputWriter:output]; 112 | [_writer writeString:@"\n"]; 113 | 114 | [_writer pushAttribute:@"xmlns" value:@"http://www.sitemaps.org/schemas/sitemap/0.9"]; 115 | [_writer startElement:@"sitemapindex"]; 116 | } 117 | 118 | return self; 119 | } 120 | 121 | - (void)writeSitemapWithLocation:(NSURL *)loc modificationDate:(NSDate *)lastMod; 122 | { 123 | [_writer writeElement:@"sitemap" content:^{ 124 | [_writer writeElement:@"loc" text:[loc absoluteString]]; 125 | 126 | if (lastMod) 127 | { 128 | #pragma clang diagnostic push 129 | #pragma clang diagnostic ignored "-Wdeprecated-declarations" 130 | NSString *lastModText = [lastMod descriptionWithCalendarFormat:@"%Y-%m-%dT%H:%M:%SZ" 131 | timeZone:[NSTimeZone timeZoneWithAbbreviation:@"GMT"] 132 | locale:nil]; 133 | #pragma clang diagnostic pop 134 | [_writer writeElement:@"lastmod" text:lastModText]; 135 | } 136 | }]; 137 | } 138 | 139 | - (void)close; 140 | { 141 | [_writer endElement]; // 142 | [_writer release]; _writer = nil; 143 | } 144 | 145 | - (void)dealloc 146 | { 147 | [self close]; // releases _writer 148 | [super dealloc]; 149 | } 150 | 151 | @end -------------------------------------------------------------------------------- /KSHTMLWriter.h: -------------------------------------------------------------------------------- 1 | // 2 | // KSHTMLWriter.h 3 | // 4 | // Created by Mike Abdullah 5 | // Copyright © 2010 Karelia Software 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 | 26 | #import "KSXMLWriter.h" 27 | 28 | 29 | extern NSString *KSHTMLDoctypeHTML_4_01_Strict; 30 | extern NSString *KSHTMLDoctypeHTML_4_01_Transitional; 31 | extern NSString *KSHTMLDoctypeHTML_4_01_Frameset; 32 | extern NSString *KSHTMLDoctypeXHTML_1_0_Strict; 33 | extern NSString *KSHTMLDoctypeXHTML_1_0_Transitional; 34 | extern NSString *KSHTMLDoctypeXHTML_1_0_Frameset; 35 | extern NSString *KSHTMLDoctypeXHTML_1_1; 36 | extern NSString *KSHTMLDoctypeHTML_5; 37 | 38 | 39 | /** 40 | New instances are given a doctype of \c KSHTMLDoctypeHTML_5 by default. 41 | */ 42 | @interface KSHTMLWriter : KSXMLWriter 43 | { 44 | @private 45 | NSMutableArray *_classNames; 46 | } 47 | 48 | #pragma mark DTD 49 | 50 | /** 51 | Whether empty elements should be written as or . There's no setter method, as is 52 | derived from \c docType 53 | */ 54 | @property(nonatomic, readonly) BOOL isXHTML; 55 | + (BOOL)isDoctypeXHTML:(NSString *)docType; 56 | 57 | /** 58 | Overrides \c super to make the \c doctype declaration lowercase as recommended by http://html5boilerplate.com 59 | */ 60 | - (void)writeDoctypeDeclaration; 61 | 62 | 63 | #pragma mark CSS Class Name 64 | // Class names are accumulated and written automatically as an attribute of the next element started 65 | // You can also push a class name using -pushAttribute:value: if attribute is 'class' 66 | - (void)pushClassName:(NSString *)className; 67 | - (void)pushClassNames:(NSArray *)classNames; 68 | 69 | 70 | #pragma mark HTML Fragments 71 | // Any newlines in the HTML will be adjusted to account for current indentation level, but that's all 72 | // Terminating newline character will be added or removed if needed, as according to terminatingNewline argument 73 | - (void)writeHTMLString:(NSString *)html withTerminatingNewline:(BOOL)terminatingNewline; 74 | - (void)writeHTMLString:(NSString *)html; 75 | - (void)writeHTMLString:(NSString *)html range:(NSRange)range; // high-performance variant 76 | 77 | 78 | #pragma mark General 79 | 80 | // 81 | // Pretty standard convenience methods 82 | - (void)writeElement:(NSString *)name idName:(NSString *)idName className:(NSString *)className content:(void (^)(void))content; 83 | - (void)startElement:(NSString *)tagName className:(NSString *)className; 84 | - (void)startElement:(NSString *)tagName idName:(NSString *)idName className:(NSString *)className; 85 | 86 | 87 | #pragma mark Document 88 | /** 89 | Convenience to give you standard document structure 90 | @param headBlock Optional 91 | */ 92 | - (void)writeDocumentWithHead:(void (^)(void))headBlock body:(void (^)(void))bodyBlock; 93 | 94 | 95 | #pragma mark Line Break 96 | //
OR
97 | // depends on isXHTML 98 | - (void)writeLineBreak; 99 | 100 | 101 | #pragma mark Links 102 | // 103 | - (void)writeAnchorElementWithHref:(NSString *)href 104 | title:(NSString *)titleString 105 | target:(NSString *)targetString 106 | rel:(NSString *)relString 107 | content:(void (^)(void))content; // a block must provided - an empty anchor doesn't make sense! 108 | 109 | // Deprecated 110 | - (void)startAnchorElementWithHref:(NSString *)href title:(NSString *)titleString target:(NSString *)targetString rel:(NSString *)relString; 111 | 112 | 113 | #pragma mark Images 114 | // ... 115 | - (void)writeImageWithSrc:(NSString *)src 116 | alt:(NSString *)alt 117 | width:(id)width 118 | height:(id)height; 119 | 120 | 121 | #pragma mark Link 122 | 123 | // 124 | // Goes in to link to scripts, CSS, etc. 125 | - (void)writeLinkWithHref:(NSString *)href 126 | type:(NSString *)type 127 | rel:(NSString *)rel 128 | title:(NSString *)title 129 | media:(NSString *)media; 130 | 131 | // Note: If a title is set, it is considered an *alternate* stylesheet. http://www.alistapart.com/articles/alternate/ 132 | - (void)writeLinkToStylesheet:(NSString *)href 133 | title:(NSString *)title 134 | media:(NSString *)media; 135 | 136 | 137 | #pragma mark Scripts 138 | 139 | /** 140 | Writes a ", 102 | @"The script contents go on their own line, level with the tag goes down onto its own line too"); 104 | } 105 | 106 | /** 107 | This is an example similar to something in Sandvox where you have some stuff followed by a 108 | complicated header (i.e. it's got an element nested in it). This is the pure version which works ok 109 | as-is, but I want to keep the test around. 110 | */ 111 | - (void)testPrettyPrinting { 112 | 113 | [writer writeElement:@"div" content:^{ 114 | [writer writeElement:@"p" text:@"Text"]; 115 | }]; 116 | 117 | [writer writeElement:@"h2" content:^{ 118 | [writer writeElement:@"span" text:@"Subheading"]; 119 | }]; 120 | 121 | XCTAssertEqualObjects(output.string, 122 | @"
\n\t

Text

\n
\n" 123 | @"

Subheading

"); 124 | } 125 | 126 | /** 127 | …and now we try it again, but this time mimicking some of the content coming from a source other 128 | than nice writer commands. e.g. a template 129 | */ 130 | - (void)testPrettyPrintingAfterTemplate { 131 | 132 | // Some template stuff… 133 | [writer writeString:@"TEMPLATE START\n"]; 134 | 135 | // …contains a direct bit of content 136 | [writer writeElement:@"div" content:^{ 137 | [writer writeElement:@"h4" text:@"Title"]; 138 | }]; 139 | 140 | // Then goes back to the template 141 | [writer writeString:@"\n"]; 142 | [writer writeString:@"TEMPLATE END\n"]; 143 | 144 | // And now it's time to write the next thing 145 | [writer resetPrettyPrinting]; 146 | [writer writeElement:@"h2" content:^{ 147 | [writer writeElement:@"span" text:@"Subheading"]; 148 | }]; 149 | 150 | XCTAssertEqualObjects(output.string, 151 | @"TEMPLATE START\n" 152 | @"
\n" 153 | @"\t

Title

\n" 154 | @"
\n" 155 | @"TEMPLATE END\n" 156 | @"

Subheading

"); 157 | } 158 | 159 | @end 160 | -------------------------------------------------------------------------------- /Tests/Classes/KSXMLWriterCompoundTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // KSXMLWriterCompoundTests.m 3 | // KSHTMLWriterTests 4 | // 5 | // Created by Sam Deane on 27/01/2012. 6 | // Copyright (c) 2012 Karelia Software. All rights reserved. 7 | // 8 | 9 | #import "ECParameterisedTestCase.h" 10 | #import "KSXMLWriter.h" 11 | #import "KSHTMLWriter.h" 12 | @import KSWriter; 13 | 14 | 15 | 16 | @interface KSXMLWriterCompoundTests : ECParameterisedTestCase 17 | @end 18 | 19 | 20 | @implementation KSXMLWriterCompoundTests 21 | 22 | #pragma mark - Helpers 23 | 24 | - (void)writer:(KSXMLWriter*)writer performActions:(NSArray*)actions 25 | { 26 | for (NSDictionary* action in actions) 27 | { 28 | NSArray* content = [action objectForKey:@"content"]; 29 | NSDictionary* attributes = [action objectForKey:@"attributes"]; 30 | 31 | NSString* comment = [action objectForKey:@"comment"]; 32 | if (comment) 33 | { 34 | [writer writeComment:comment]; 35 | } 36 | 37 | NSString* text = [action objectForKey:@"text"]; 38 | if (text) 39 | { 40 | [writer writeCharacters:text]; 41 | } 42 | 43 | NSDictionary* push = [action objectForKey:@"push"]; 44 | if (push) 45 | { 46 | for (NSString* key in push) 47 | { 48 | [writer pushAttribute:key value:[attributes objectForKey:key]]; 49 | } 50 | } 51 | 52 | NSString* element = [action objectForKey:@"element"]; 53 | if (element) 54 | { 55 | [attributes enumerateKeysAndObjectsUsingBlock:^(NSString *attribute, id value, BOOL *stop) { 56 | [writer pushAttribute:attribute value:value]; 57 | }]; 58 | 59 | [writer writeElement:element content:^{ 60 | [self writer:writer performActions:content]; 61 | }]; 62 | } 63 | 64 | } 65 | } 66 | 67 | #pragma mark - Tests 68 | 69 | typedef enum 70 | { 71 | TestXML, 72 | TestHTML 73 | } TestType; 74 | 75 | - (void)testCompoundWithTestType:(TestType)type 76 | { 77 | Class class; 78 | NSString* expectedKey; 79 | 80 | switch (type) 81 | { 82 | case TestXML: 83 | class = [KSXMLWriter class]; 84 | expectedKey = @"expected-xml"; 85 | break; 86 | 87 | default: 88 | class = [KSHTMLWriter class]; 89 | expectedKey = @"expected-html"; 90 | break; 91 | } 92 | 93 | NSDictionary* test = self.parameterisedTestDataItem; 94 | KSWriter* output = [KSWriter stringWriterWithEncoding:NSUnicodeStringEncoding]; 95 | KSXMLWriter* writer = [[class alloc] initWithOutputWriter:output]; 96 | writer.prettyPrint = YES; 97 | 98 | NSArray* actions = [test objectForKey:@"actions"]; 99 | NSString* expected = [test objectForKey:expectedKey]; 100 | [self writer:writer performActions:actions]; 101 | 102 | NSString* generated = [output string]; 103 | [self assertString:generated matchesString:expected]; 104 | } 105 | 106 | - (void)parameterisedTestCompoundXML 107 | { 108 | [self testCompoundWithTestType:TestXML]; 109 | } 110 | 111 | - (void)parameterisedTestCompoundHTML 112 | { 113 | [self testCompoundWithTestType:TestHTML]; 114 | } 115 | 116 | @end 117 | -------------------------------------------------------------------------------- /Tests/Classes/KSXMLWriterTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // KSHTMLWriterTests.m 3 | // KSHTMLWriterTests 4 | // 5 | // Created by Sam Deane on 18/01/2012. 6 | // Copyright (c) 2012 Karelia Software. All rights reserved. 7 | // 8 | 9 | @import XCTest; 10 | 11 | #import "KSXMLWriter.h" 12 | @import KSWriter; 13 | 14 | 15 | @interface KSXMLWriterTests : XCTestCase 16 | { 17 | KSWriter* output; 18 | KSXMLWriter* writer; 19 | } 20 | @end 21 | 22 | 23 | #pragma mark - Unit Tests Implementation 24 | 25 | @implementation KSXMLWriterTests 26 | 27 | - (void)setUp 28 | { 29 | output = [KSWriter stringWriterWithEncoding:NSUnicodeStringEncoding]; 30 | writer = [[KSXMLWriter alloc] initWithOutputWriter:output]; 31 | } 32 | 33 | #pragma mark Single Elements 34 | 35 | - (void)testNoAction 36 | { 37 | NSString* generated = [output string]; 38 | XCTAssertTrue([generated isEqualToString:@""], @"generated string is empty"); 39 | } 40 | 41 | - (void)testWriteElementNoContent 42 | { 43 | [writer writeElement:@"foo" content:nil]; 44 | 45 | NSString* generated = [output string]; 46 | XCTAssertEqualObjects(generated, @""); 47 | } 48 | 49 | - (void)testWriteElementEmptyContent 50 | { 51 | [writer writeElement:@"foo" content:^{ 52 | }]; 53 | 54 | NSString* generated = [output string]; 55 | XCTAssertEqualObjects(generated, @""); 56 | } 57 | 58 | - (void)testWriteElementNoAttributes 59 | { 60 | [writer writeElement:@"foo" content:^{ 61 | [writer writeCharacters:@"bar"]; 62 | }]; 63 | 64 | NSString* generated = [output string]; 65 | XCTAssertEqualObjects(generated, @"bar"); 66 | } 67 | 68 | - (void)testWriteElementOneAttribute 69 | { 70 | [writer writeElement:@"foo" content:^{ 71 | [writer addAttribute:@"wobble" value:@"wibble"]; 72 | 73 | [writer writeCharacters:@"bar"]; 74 | }]; 75 | 76 | NSString* generated = [output string]; 77 | XCTAssertEqualObjects(generated, @"bar"); 78 | } 79 | 80 | - (void)testWriteElementMultipleAttributes 81 | { 82 | [writer writeElement:@"foo" content:^{ 83 | [writer addAttribute:@"k2" value:@"o2"]; 84 | [writer addAttribute:@"k1" value:@"o1"]; 85 | 86 | [writer writeCharacters:@"bar"]; 87 | }]; 88 | 89 | NSString* generated = [output string]; 90 | XCTAssertEqualObjects(generated, @"bar"); 91 | } 92 | 93 | - (void)testAddingAttributeTooEarly { 94 | 95 | XCTAssertThrows([writer addAttribute:@"foo" value:@"bar"]); 96 | } 97 | 98 | - (void)testAddingAttributeTooLate { 99 | 100 | [writer writeElement:@"foo" content:^{ 101 | [writer writeCharacters:@"text"]; 102 | 103 | XCTAssertThrows([writer addAttribute:@"foo" value:@"bar"]); 104 | }]; 105 | } 106 | 107 | - (void)testEmptyElementWithAttribute 108 | { 109 | [writer writeElement:@"foo" content:^{ 110 | [writer addAttribute:@"wobble" value:@"wibble"]; 111 | }]; 112 | 113 | NSString* generated = [output string]; 114 | XCTAssertEqualObjects(generated, @""); 115 | } 116 | 117 | - (void)testPushAttribute 118 | { 119 | [writer pushAttribute:@"a1" value:@"v1"]; 120 | XCTAssertTrue([writer hasCurrentAttributes]); 121 | XCTAssertNotNil([writer currentAttributes]); 122 | NSUInteger attributeCount = [[writer currentAttributes] count]; 123 | XCTAssertEqual(attributeCount, (NSUInteger) 1 , @"wrong number of attributes"); 124 | 125 | [writer pushAttribute:@"a2" value:@"v2"]; 126 | attributeCount = [[writer currentAttributes] count]; 127 | XCTAssertEqual(attributeCount, (NSUInteger) 2, @"wrong number of attributes"); 128 | 129 | [writer writeElement:@"foo" content:^{ 130 | [writer writeCharacters:@"bar"]; 131 | }]; 132 | 133 | NSString* generated = [output string]; 134 | XCTAssertEqualObjects(generated, @"bar"); 135 | 136 | XCTAssertFalse([writer hasCurrentAttributes], @"has attributes"); 137 | XCTAssertNotNil([writer currentAttributes], @"has attributes"); 138 | attributeCount = [[writer currentAttributes] count]; 139 | XCTAssertEqual(attributeCount, (NSUInteger) 0, @"wrong number of attributes"); 140 | } 141 | 142 | - (void)testWriteEscapedEntities 143 | { 144 | // TODO could expand this to include a list of all entities 145 | [writer writeElement:@"foo" content:^{ 146 | [writer writeCharacters:@"< & >"]; 147 | }]; 148 | 149 | NSString* generated = [output string]; 150 | XCTAssertEqualObjects(generated, @"< & >"); 151 | 152 | // test the raw escaping whilst we're at it 153 | NSString* escaped = [KSXMLWriter stringFromCharacters:@"< & >"]; 154 | XCTAssertEqualObjects(escaped, @"< & >"); 155 | } 156 | 157 | - (void)testWriteEscapedNonAsciiCharacters 158 | { 159 | output = [KSWriter stringWriterWithEncoding:NSASCIIStringEncoding]; 160 | writer = [[KSXMLWriter alloc] initWithOutputWriter:output]; 161 | 162 | // TODO could expand this to loop through all characters, but some of them will expand 163 | // to unexpected things - e.g. see character 160 below... 164 | 165 | [writer writeElement:@"foo" content:^{ 166 | 167 | // write some random non-ascii characters 168 | // (160 happens to be a non-breaking space, so it will be encoded as nbsp;) 169 | static char nonAsciiChars[] = { 160, 180, 200, 0 }; 170 | NSString* nonAscii = [NSString stringWithCString:nonAsciiChars encoding:NSISOLatin1StringEncoding]; 171 | [writer writeCharacters:nonAscii]; 172 | }]; 173 | 174 | NSString* generated = [output string]; 175 | XCTAssertEqualObjects(generated, @" ´È"); 176 | 177 | } 178 | 179 | - (void)testWriteComment 180 | { 181 | // TODO could expand this to include a list of all entities 182 | [writer writeElement:@"foo" content:^{ 183 | [writer writeComment:@"this is a comment"]; 184 | [writer writeCharacters:@"this is not a comment"]; 185 | [writer writeComment:@"this is another comment"]; 186 | }]; 187 | 188 | NSString* generated = [output string]; 189 | XCTAssertEqualObjects(generated, @"this is not a comment"); 190 | } 191 | 192 | - (void)testStartDocument 193 | { 194 | writer.doctype = @"some-type"; 195 | [writer writeDoctypeDeclaration]; 196 | [writer writeElement:@"foo" content:^{ 197 | [writer writeCharacters:@"bar"]; 198 | }]; 199 | 200 | NSString* generated = [output string]; 201 | XCTAssertEqualObjects(generated, @"\nbar"); 202 | 203 | } 204 | 205 | #pragma mark Multiple Elements 206 | 207 | - (void)testSiblingElements { 208 | [writer writeElement:@"foo" content:nil]; 209 | [writer writeElement:@"bar" content:nil]; 210 | 211 | XCTAssertEqualObjects(output.string, @""); 212 | } 213 | 214 | - (void)testSiblingElementsPrettyPrinted { 215 | writer.prettyPrint = YES; 216 | 217 | [writer writeElement:@"foo" content:nil]; 218 | [writer writeElement:@"bar" content:nil]; 219 | 220 | XCTAssertEqualObjects(output.string, @"\n"); 221 | } 222 | 223 | - (void)testNestedElements { 224 | 225 | [writer writeElement:@"foo" content:^{ 226 | [writer writeElement:@"bar" content:nil]; 227 | }]; 228 | 229 | XCTAssertEqualObjects(output.string, @""); 230 | } 231 | 232 | - (void)testNestedElementsPrettyPrinted { 233 | writer.prettyPrint = YES; 234 | 235 | [writer writeElement:@"foo" content:^{ 236 | [writer writeElement:@"bar" content:nil]; 237 | }]; 238 | 239 | XCTAssertEqualObjects(output.string, @"\n\t\n"); 240 | } 241 | 242 | @end 243 | -------------------------------------------------------------------------------- /Tests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | com.karelia.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /Tests/KSHTMLWriterFramework_Tests.m: -------------------------------------------------------------------------------- 1 | // 2 | // KSHTMLWriterFramework_Tests.m 3 | // KSHTMLWriterFramework Tests 4 | // 5 | // Created by Mike on 12/05/2015. 6 | // Copyright (c) 2015 Karelia Software. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface KSHTMLWriterFramework_Tests : XCTestCase 13 | 14 | @end 15 | 16 | @implementation KSHTMLWriterFramework_Tests 17 | 18 | - (void)setUp { 19 | [super setUp]; 20 | // Put setup code here. This method is called before the invocation of each test method in the class. 21 | } 22 | 23 | - (void)tearDown { 24 | // Put teardown code here. This method is called after the invocation of each test method in the class. 25 | [super tearDown]; 26 | } 27 | 28 | - (void)testExample { 29 | // This is an example of a functional test case. 30 | XCTAssert(YES, @"Pass"); 31 | } 32 | 33 | - (void)testPerformanceExample { 34 | // This is an example of a performance test case. 35 | [self measureBlock:^{ 36 | // Put the code you want to measure the time of here. 37 | }]; 38 | } 39 | 40 | @end 41 | -------------------------------------------------------------------------------- /Tests/README.mdown: -------------------------------------------------------------------------------- 1 | To run these tests, choose "Test" from the "Product" menu. 2 | 3 | Basic Tests 4 | ----------- 5 | 6 | There are some basic tests exercising the following classes: 7 | 8 | - KSXMLWriter 9 | - (see also the tests in the KSWriter project) 10 | 11 | Compound XML Tests 12 | ------------------ 13 | 14 | In addition to the basic tests, there are also some "compound" xml tests. These test various permutations of 15 | writer calls to see if they generate the correct XML or HTML. 16 | 17 | The tests are driven from a dictionary of NSDictionary based structures, each of which 18 | describes a sequence of actions to perform to build up the xml/html. The actions include 19 | 20 | - add a comment 21 | - add some text 22 | - add some attributes to the current element 23 | - add a nested element 24 | 25 | The NSDictionary also includes the expected value of the generated XML and HTML, so that the test can check it. 26 | 27 | In theory the NSDictionaries that drive the tests could be auto-generated to cover many permutations. The 28 | only problem with that idea is that the expected output would also have to be generated, which leads to a 29 | case of the chicken-egg problem (since generating XML is the thing we're trying to test in the 30 | first place). 31 | 32 | At the moment, the NSDictionaries are hand-written, and are read from the "Compound Tests.plist" file. 33 | 34 | 35 | "Snippets" Tests 36 | ---------------- 37 | 38 | These tests run through directories full of hmtl "snippets", performing some action on each snippet 39 | and then checking it against an expected outcome. 40 | 41 | Before running any of these tests the framework loads a stub html document into a hidden web view. 42 | The stub document (defined in Stub.html) is just an empty web page. 43 | 44 | The snippet tests use this web view as the host for testing each html stub. They first inject the stub into the 45 | document using WebKit DOM methods. They then grab back the DOM representation of the stub, and use a HSHTMLDOMAdaptor 46 | to write it back out as XML/HTML. 47 | 48 | This actual output is then compared against an expected output. 49 | 50 | There are two types of snippets that are tested. 51 | 52 | The normal snippets (located in Snippets/Normal) are expected to be unchanged by a round trip through the KSHTMLWriter. 53 | 54 | The pretty print snippets (located in Snippets/Pretty) are expected to be cleaned up when pretty-printing is enabled. For 55 | these snippets, each item of the folder is a sub-folder containing two files: input.html and output.html. The input file 56 | is injected into the stub document, and the output is then compared against the output file. 57 | 58 | This test fails if any KSHTMLWriter fails to generate identical output for any of the snippets. 59 | It is currently whitespace sensitive and does an exact string comparison, which may be overkill. 60 | 61 | 62 | A Note About Dynamic Test Methods 63 | ================================= 64 | 65 | Anyone familiar with the unit testing framework we're using (OCUnit/SenTestKit) will know that tests are generally defined 66 | as simple void methods taking no parameters, which get run once. 67 | 68 | These are fine in most cases, but there are some tests in our suite where we want to apply the same action repeatedly to 69 | each file in a folder, or each item in a plist. We could do this in a loop inside the test, but then if any item in the 70 | loop failed, we'd just see that the whole test had failed, which isn't that helpful. 71 | 72 | So for these cases we do a little bit of extra work to ensure that the result of the testing of each file or plist item is 73 | reported as a separate result. 74 | 75 | This gives us the change to see slightly nicer output if something goes wrong, and also a clearer idea about how many tests 76 | are actually run if everything goes well, since each iteration of each loop is counted as a test. 77 | 78 | Because we effectively run the same test repeatedly, the default name of the test (which is the method name) isn't very useful 79 | in the final reports, since it just appears multiple times, leading to this sort of thing: 80 | 81 | Test Case '-[KSXMLWriterCompoundTests testCompound]' started. 82 | Test Case '-[KSXMLWriterCompoundTests testCompound]' passed (0.001 seconds). 83 | Test Case '-[KSXMLWriterCompoundTests testCompound]' started. 84 | Test Case '-[KSXMLWriterCompoundTests testCompound]' passed (0.001 seconds). 85 | Test Case '-[KSXMLWriterCompoundTests testCompound]' started. 86 | Test Case '-[KSXMLWriterCompoundTests testCompound]' passed (0.001 seconds). 87 | 88 | To try to improve this reporting, we override the name method of the test framework and attempt to return something more descriptive. 89 | Unfortunately, OCUnit seems to expect this name to be in a very rigid format, essentially: "-[Class method]". 90 | 91 | Any attempt to return something in a different format appears to break Xcode's reporting of the tests. As a result, our options for 92 | messing about with the name are somewhat limited. Essentially what we do is to replace the method name with a single word. 93 | 94 | In the case where we're iterating over files, we can use the file name, which results in something reasonably descriptive (note that 95 | this means that the test files need to have single word names). 96 | 97 | In the case where we're iterating over items in a plist, we use the key of each item as its name. -------------------------------------------------------------------------------- /Tests/Resources/KSHTMLWriterTests.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | com.karelia.${PRODUCT_NAME:rfc1034identifier} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Tests/Resources/KSXMLWriterCompoundTests.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AttributeWithComments 6 | 7 | actions 8 | 9 | 10 | element 11 | foo 12 | attributes 13 | 14 | test-attribute 15 | test value 16 | 17 | content 18 | 19 | 20 | comment 21 | a comment 22 | 23 | 24 | text 25 | bar 26 | 27 | 28 | comment 29 | another comment 30 | 31 | 32 | 33 | 34 | expected-xml 35 | <foo test-attribute="test value"><!--a comment-->bar<!--another comment--></foo> 36 | expected-html 37 | <foo test-attribute="test value"><!--a comment-->bar<!--another comment--></foo> 38 | 39 | SingleComment 40 | 41 | actions 42 | 43 | 44 | comment 45 | just a comment 46 | 47 | 48 | expected-xml 49 | <!--just a comment--> 50 | expected-html 51 | <!--just a comment--> 52 | 53 | JustText 54 | 55 | actions 56 | 57 | 58 | text 59 | just some text 60 | 61 | 62 | expected-xml 63 | just some text 64 | expected-html 65 | just some text 66 | 67 | EmptyElement 68 | 69 | actions 70 | 71 | 72 | element 73 | empty-element 74 | 75 | 76 | expected-xml 77 | <empty-element /> 78 | expected-html 79 | <empty-element></empty-element> 80 | 81 | CommentAttributeComment 82 | 83 | actions 84 | 85 | 86 | comment 87 | blah 88 | 89 | 90 | element 91 | foo 92 | attributes 93 | 94 | test-attribute 95 | test value 96 | 97 | content 98 | 99 | 100 | comment 101 | a comment 102 | 103 | 104 | text 105 | bar 106 | element 107 | sub 108 | content 109 | 110 | 111 | comment 112 | a comment 113 | 114 | 115 | text 116 | bar 117 | 118 | 119 | comment 120 | another comment 121 | 122 | 123 | 124 | 125 | comment 126 | another comment 127 | 128 | 129 | 130 | 131 | comment 132 | blah 133 | 134 | 135 | expected-xml 136 | <!--blah--><foo test-attribute="test value"><!--a comment-->bar 137 | <sub><!--a comment-->bar<!--another comment--></sub><!--another comment--> 138 | </foo><!--blah--> 139 | expected-html 140 | <!--blah--><foo test-attribute="test value"><!--a comment-->bar<sub><!--a comment-->bar<!--another comment--></sub><!--another comment--></foo><!--blah--> 141 | 142 | CompoundElement 143 | 144 | actions 145 | 146 | 147 | element 148 | foo 149 | content 150 | 151 | 152 | element 153 | foo 154 | content 155 | 156 | 157 | text 158 | text 159 | 160 | 161 | 162 | 163 | 164 | 165 | expected-xml 166 | <foo> 167 | <foo>text</foo> 168 | </foo> 169 | expected-html 170 | <foo> 171 | <foo>text</foo> 172 | </foo> 173 | 174 | ElementContainingEmptyElement 175 | 176 | actions 177 | 178 | 179 | element 180 | foo 181 | content 182 | 183 | 184 | element 185 | foo 186 | 187 | 188 | 189 | 190 | expected-xml 191 | <foo> 192 | <foo /> 193 | </foo> 194 | expected-html 195 | <foo> 196 | <foo></foo> 197 | </foo> 198 | 199 | ThreeNestedElements 200 | 201 | actions 202 | 203 | 204 | element 205 | foo 206 | content 207 | 208 | 209 | element 210 | foo 211 | content 212 | 213 | 214 | element 215 | foo 216 | content 217 | 218 | 219 | text 220 | text 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | expected-xml 230 | <foo> 231 | <foo> 232 | <foo>text</foo> 233 | </foo> 234 | </foo> 235 | expected-html 236 | <foo> 237 | <foo> 238 | <foo>text</foo> 239 | </foo> 240 | </foo> 241 | 242 | EmptyElementNestedTwoElementsDeep 243 | 244 | actions 245 | 246 | 247 | element 248 | foo 249 | content 250 | 251 | 252 | element 253 | foo 254 | content 255 | 256 | 257 | element 258 | foo 259 | 260 | 261 | 262 | 263 | 264 | 265 | expected-xml 266 | <foo> 267 | <foo> 268 | <foo /> 269 | </foo> 270 | </foo> 271 | expected-html 272 | <foo> 273 | <foo> 274 | <foo></foo> 275 | </foo> 276 | </foo> 277 | 278 | TwoElementsInElement 279 | 280 | actions 281 | 282 | 283 | element 284 | foo 285 | content 286 | 287 | 288 | element 289 | foo 290 | content 291 | 292 | 293 | text 294 | text 295 | 296 | 297 | 298 | 299 | element 300 | foo 301 | content 302 | 303 | 304 | text 305 | text 306 | 307 | 308 | 309 | 310 | 311 | 312 | expected-xml 313 | <foo> 314 | <foo>text</foo> 315 | <foo>text</foo> 316 | </foo> 317 | expected-html 318 | <foo> 319 | <foo>text</foo> 320 | <foo>text</foo> 321 | </foo> 322 | 323 | TwoEmptyElementsInElement 324 | 325 | actions 326 | 327 | 328 | element 329 | foo 330 | content 331 | 332 | 333 | element 334 | foo 335 | 336 | 337 | element 338 | foo 339 | 340 | 341 | 342 | 343 | expected-xml 344 | <foo> 345 | <foo /> 346 | <foo /> 347 | </foo> 348 | expected-html 349 | <foo> 350 | <foo></foo> 351 | <foo></foo> 352 | </foo> 353 | 354 | ElementsWithTextAtMultipleLevels 355 | 356 | actions 357 | 358 | 359 | element 360 | foo 361 | content 362 | 363 | 364 | element 365 | foo 366 | content 367 | 368 | 369 | text 370 | text 371 | 372 | 373 | 374 | 375 | element 376 | foo 377 | content 378 | 379 | 380 | element 381 | foo 382 | content 383 | 384 | 385 | text 386 | text 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | expected-xml 396 | <foo> 397 | <foo>text</foo> 398 | <foo> 399 | <foo>text</foo> 400 | </foo> 401 | </foo> 402 | expected-html 403 | <foo> 404 | <foo>text</foo> 405 | <foo> 406 | <foo>text</foo> 407 | </foo> 408 | </foo> 409 | 410 | 411 | 412 | -------------------------------------------------------------------------------- /Tests/Resources/Stub.html: -------------------------------------------------------------------------------- 1 | Test Web Page
Content goes here
-------------------------------------------------------------------------------- /Tests/Scripts/test.sh: -------------------------------------------------------------------------------- 1 | echo Testing Mac 2 | 3 | base=`dirname $0` 4 | common="$base/../ECUnitTests/Scripts/" 5 | source "$common/test-common.sh" 6 | 7 | # build & run the tests 8 | xcodebuild -target "KSHTMLWriterTests" -configuration $testConfig -sdk "$testSDKMac" $testOptions | "$common/$testConvertOutput" 9 | -------------------------------------------------------------------------------- /Tests/Stringification.testdata: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Input" : "

Text <\/p>", 4 | "Literal" : "

Text <\/p>", 5 | "Pretty-printed" : "

Text<\/p>" 6 | }, 7 | { 8 | "Input" : "

Text<\/p>", 9 | "Literal" : "

Text<\/p>", 10 | "Pretty-printed" : "

Text<\/p>" 11 | }, 12 | { 13 | "Input" : "

Text <\/p>", 14 | "Literal" : "

Text <\/p>", 15 | "Pretty-printed" : "

Text<\/p>" 16 | }, 17 | { 18 | "Input" : "

Foo bar<\/b> baz<\/p>", 19 | "Literal" : "

Foo bar<\/b> baz<\/p>", 20 | "Pretty-printed" : "

Foo bar<\/b> baz<\/p>" 21 | }, 22 | { 23 | "Input" : "

Foo bar<\/b> baz <\/p>", 24 | "Literal" : "

Foo bar<\/b> baz <\/p>", 25 | "Pretty-printed" : "

Foo bar<\/b> baz<\/p>" 26 | }, 27 | { 28 | "Input" : "\t\t\t\t\t

Text<\/p>", 29 | "Literal" : "\t\t\t\t\t

Text<\/p>", 30 | "Pretty-printed" : "

Text<\/p>" 31 | }, 32 | { 33 | "Input" : "\n

Text 1<\/p>\n\n

Text 2<\/p>\n", 34 | "Literal" : "\n

Text 1<\/p>\n\n

Text 2<\/p>\n", 35 | "Pretty-printed" : "

Text 1<\/p>\n

Text 2<\/p>" 36 | }, 37 | { 38 | "Input" : "

\n This is a test<\/strong>\n<\/div>", 39 | "Literal" : "
\n This is a test<\/strong>\n<\/div>", 40 | "Pretty-printed" : "
This is a test<\/strong><\/div>" 41 | }, 42 | { 43 | "Input" : "Text<\/a>", 44 | "Literal" : "Text<\/a>", 45 | "Pretty-printed" : "Text<\/a>" 46 | }, 47 | { 48 | "Input" : "Text<\/span>", 49 | "Literal" : "Text<\/span>", 50 | "Pretty-printed" : "Text<\/span>" 51 | }, 52 | { 53 | "Input" : "Text<\/span>", 54 | "Literal" : "Text<\/span>", 55 | "Pretty-printed" : "Text<\/span>" 56 | } 57 | ] -------------------------------------------------------------------------------- /Tests/StringificationTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // KSHTMLWriter 3 | // 4 | // Created by Mike on 13/05/2015. 5 | // Copyright (c) 2015 Karelia Software. All rights reserved. 6 | // 7 | 8 | #import "KSXMLWriterDOMAdaptor.h" 9 | #import "KSHTMLWriter.h" 10 | #import 11 | 12 | 13 | @interface StringificationTests : XCTestCase 14 | 15 | @end 16 | 17 | 18 | @implementation StringificationTests { 19 | NSArray *_suite; 20 | WebView *_webView; 21 | } 22 | 23 | - (void)setUp { 24 | [super setUp]; 25 | 26 | NSURL *url = [[NSBundle bundleForClass:self.class] URLForResource:@"Stringification" withExtension:@"testdata"]; 27 | XCTAssertNotNil(url); 28 | _suite = [NSJSONSerialization JSONObjectWithData:[NSData dataWithContentsOfURL:url] options:0 error:NULL]; 29 | XCTAssertNotNil(_suite); 30 | 31 | _webView = [[WebView alloc] init]; 32 | } 33 | 34 | - (void)tearDown { 35 | // Put teardown code here. This method is called after the invocation of each test method in the class. 36 | [super tearDown]; 37 | } 38 | 39 | - (void)loadHTMLString:(NSString *)string completionHandler:(void (^)())block { 40 | 41 | // Explicitly supply our own tag, so whitespace within it is preserved in the DOM 42 | string = [NSString stringWithFormat:@"%@", string]; 43 | 44 | [_webView.mainFrame loadHTMLString:string baseURL:nil]; 45 | [self expectationForNotification:WebViewProgressFinishedNotification object:_webView handler:NULL]; 46 | [self waitForExpectationsWithTimeout:5 handler:NULL]; 47 | block(); 48 | } 49 | 50 | - (void)testItAll { 51 | for (NSDictionary *properties in _suite) { 52 | 53 | NSString *input = properties[@"Input"]; 54 | NSString *literalOutput = properties[@"Literal"]; 55 | NSString *prettyPrintedOutput = properties[@"Pretty-printed"]; 56 | 57 | [self loadHTMLString:input completionHandler:^{ 58 | 59 | // Suck the HTML back out of the DOM and make sure we've done a good job of that 60 | DOMHTMLElement *body = _webView.mainFrame.DOMDocument.body; 61 | KSWriter *buffer = [KSWriter stringWriterWithEncoding:NSUTF8StringEncoding]; 62 | KSHTMLWriter *writer = [[KSHTMLWriter alloc] initWithOutputWriter:buffer]; 63 | 64 | KSXMLWriterDOMAdaptor *stringifier = [[KSXMLWriterDOMAdaptor alloc] initWithXMLWriter:writer options:0]; 65 | [stringifier writeInnerOfDOMNode:body]; 66 | 67 | XCTAssertEqualObjects(buffer.string, literalOutput); 68 | 69 | 70 | buffer = [KSWriter stringWriterWithEncoding:NSUTF8StringEncoding]; 71 | writer = [[KSHTMLWriter alloc] initWithOutputWriter:buffer]; 72 | 73 | stringifier = [[KSXMLWriterDOMAdaptor alloc] initWithXMLWriter:writer options:KSXMLWriterDOMAdaptorPrettyPrint]; 74 | [stringifier writeInnerOfDOMNode:body]; 75 | 76 | XCTAssertEqualObjects(buffer.string, prettyPrintedOutput); 77 | }]; 78 | } 79 | 80 | } 81 | 82 | @end 83 | --------------------------------------------------------------------------------