├── .gitignore ├── LICENSE ├── NSJSONSerialization+Comments.h ├── README.md └── NSJSONSerialization+Comments.m /.gitignore: -------------------------------------------------------------------------------- 1 | # CocoaPods 2 | # 3 | # We recommend against adding the Pods directory to your .gitignore. However 4 | # you should judge for yourself, the pros and cons are mentioned at: 5 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control? 6 | # 7 | # Pods/ 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Alexander Blach 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NSJSONSerialization+Comments.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSJSONSerialization+Comments.h 3 | // ABCodeEditor 4 | // 5 | // Created by Alexander Blach on 22.07.14. 6 | // Copyright (c) 2014 Alexander Blach. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSJSONSerialization (Comments) 12 | 13 | + (NSData *)dataByStrippingJSONCommentsAndWhiteSpaceOfUTF8Data:(NSData *)data 14 | skipBytes:(NSUInteger)bytesToSkip; 15 | 16 | // preferred method, since it doesn't need to convert an NSString if the data is encoded with UTF-8 17 | + (id)JSONObjectWithCommentedData:(NSData *)data 18 | options:(NSJSONReadingOptions)opt 19 | error:(NSError **)error; 20 | 21 | + (id)JSONObjectWithCommentedContentsOfURL:(NSURL *)url 22 | options:(NSJSONReadingOptions)opt 23 | error:(NSError **)error; 24 | 25 | + (id)JSONObjectWithCommentedContentsOfFile:(NSString *)path 26 | options:(NSJSONReadingOptions)opt 27 | error:(NSError **)error; 28 | 29 | + (id)JSONObjectWithCommentedString:(NSString *)string 30 | options:(NSJSONReadingOptions)opt 31 | error:(NSError **)error; 32 | 33 | 34 | // convenience method if you need an NSString instead of NSData 35 | + (NSString *)stringWithJSONObject:(id)obj options:(NSJSONWritingOptions)opt error:(NSError **)error; 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NSJSONSerialization+Comments 2 | 3 | This Objective-C category adds C style comment support to NSJSONSerialization. It allows you to read JSON files with `//` and `/* … */` comments. It is similar to JSONKit's `JKParseOptionComments` option. 4 | 5 | # Usage 6 | 7 | You can use this category's `JSONObjectWithCommentedData:options:error:` method just like NSJSONSerialization's `JSONObjectWithData:options:error:`: 8 | 9 | ```Objective-C 10 | id object = [NSJSONSerialization JSONObjectWithCommentedData:data options:0 error:&error]; 11 | ``` 12 | 13 | It strips single line and multi-line comments as well as whitespace before it hands over the data to NSJSONSerialization. 14 | 15 | The code works directly on UTF-8 data without converting it to an NSString, but also detects UTF-16 and UTF-32 byte order marks (BOM). If a non-UTF-8 BOM is detected, it converts the data to UTF-8. The most efficient encoding to use for parsing is UTF-8. 16 | 17 | I've also added the convenience methods `JSONObjectWithCommentedContentsOfURL:options:error:` and `JSONObjectWithCommentedContentsOfFile:options:error:` to load JSON directly from a file or url. 18 | 19 | Finally `stringWithJSONObject:options:error:` is a convenience method around `dataWithJSONObject:options:error:` if you need an NSString instead of NSData. 20 | 21 | Have a look at [this blog post](http://blach.io/2014/07/28/nsjsonserialization-category-to-read-json-with-comments/) to read more about the motivation behind NSJSONSerialization+Comments. 22 | 23 | ## ARC Support 24 | 25 | If you are including NSJSONSerialization+Comments in a project that has Automatic Reference Counting (ARC) enabled, you will need to set the `-fno-objc-arc` compiler flag. To do this in Xcode, go to your active target and select the "Build Phases" tab and open the "Compile Sources" section. In the "Compiler Flags" column, set `-fno-objc-arc` for `NSJSONSerialization+Comments.m`. 26 | 27 | # License 28 | 29 | Code in this repository is licensed under the MIT license (see the LICENSE file). -------------------------------------------------------------------------------- /NSJSONSerialization+Comments.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSJSONSerialization+Comments.m 3 | // ABCodeEditor 4 | // 5 | // Created by Alexander Blach on 22.07.14. 6 | // Copyright (c) 2014 Alexander Blach. All rights reserved. 7 | // 8 | 9 | #import "NSJSONSerialization+Comments.h" 10 | 11 | 12 | static const int EncLen_UTF8[256] = { 13 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 14 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 15 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 16 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 17 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 18 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 19 | 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 20 | 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 1, 1}; 21 | 22 | static inline void copyUTF8CharacterAndAdvancePointers(UTF8Char **source, UTF8Char **target) { 23 | UTF8Char character = **source; 24 | if (__builtin_expect(character < 128, 1)) { 25 | // one byte UTF-8 character 26 | **target = **source; 27 | *source += 1; 28 | *target += 1; 29 | } else { 30 | int len = EncLen_UTF8[character]; 31 | memcpy(*target, *source, len); 32 | *source += len; 33 | *target += len; 34 | } 35 | } 36 | 37 | static inline void skipUTF8Character(UTF8Char **source) { *source += EncLen_UTF8[**source]; } 38 | 39 | 40 | @implementation NSJSONSerialization (Comments) 41 | 42 | + (NSData *)dataByStrippingJSONCommentsAndWhiteSpaceOfUTF8Data:(NSData *)data 43 | skipBytes:(NSUInteger)bytesToSkip { 44 | UTF8Char *originalString = (UTF8Char *)[data bytes]; 45 | NSUInteger length = [data length]; 46 | 47 | UTF8Char *modifiedString = malloc(sizeof(UTF8Char) * length); 48 | 49 | UTF8Char *originalStringCurrent = originalString; 50 | UTF8Char *originalStringEnd = originalString + length; 51 | UTF8Char *modifiedStringCurrent = modifiedString; 52 | 53 | 54 | // skip bytes 55 | originalStringCurrent += bytesToSkip; 56 | 57 | while (originalStringCurrent < originalStringEnd) { 58 | UTF8Char currentChar = *originalStringCurrent; 59 | 60 | if (currentChar == '\t' || currentChar == ' ' || currentChar == '\r' 61 | || currentChar == '\n') { 62 | // skip whitespace 63 | 64 | // Ignore whitespace tokens. According to ES 5.1 section 15.12.1.1, 65 | // whitespace tokens include tabs, carriage returns, line feeds, and 66 | // space characters. 67 | originalStringCurrent++; 68 | } else if (currentChar == '"') { 69 | // we found a string! -> handle it 70 | *modifiedStringCurrent++ = currentChar; 71 | originalStringCurrent++; 72 | 73 | UTF8Char lastChar = 0; 74 | 75 | while (originalStringCurrent < originalStringEnd) { 76 | currentChar = *originalStringCurrent; 77 | 78 | if (currentChar == '"') { 79 | *modifiedStringCurrent++ = currentChar; 80 | originalStringCurrent++; 81 | 82 | if (lastChar == '\\') { 83 | // was escaped character -> not at string end 84 | } else { 85 | // arrived at end of string 86 | break; 87 | } 88 | } else if (currentChar == '\n' || currentChar == '\r') { 89 | // line breaks should not happen in JSON strings! 90 | *modifiedStringCurrent++ = currentChar; 91 | originalStringCurrent++; 92 | break; 93 | } else { 94 | // still in string -> copy character 95 | copyUTF8CharacterAndAdvancePointers(&originalStringCurrent, 96 | &modifiedStringCurrent); 97 | } 98 | lastChar = currentChar; 99 | } 100 | } else if (currentChar == '/' && originalStringCurrent + 1 < originalStringEnd) { 101 | // maybe we have a single-line or multi-line comment 102 | UTF8Char nextChar = *(originalStringCurrent + 1); 103 | 104 | if (nextChar == '/') { 105 | // single line comment 106 | originalStringCurrent += 2; 107 | 108 | while (originalStringCurrent < originalStringEnd) { 109 | char currentChar = *originalStringCurrent; 110 | 111 | if (currentChar == '\r' || currentChar == '\n') { 112 | // at end of line -> comment end 113 | break; 114 | } else { 115 | // skip 116 | skipUTF8Character(&originalStringCurrent); 117 | } 118 | } 119 | } else if (nextChar == '*') { 120 | // multi line comment 121 | originalStringCurrent += 2; 122 | 123 | while (originalStringCurrent < originalStringEnd) { 124 | char currentChar = *originalStringCurrent; 125 | 126 | if (currentChar == '*') { 127 | originalStringCurrent++; 128 | 129 | if (originalStringCurrent < originalStringEnd) { 130 | currentChar = *originalStringCurrent; 131 | if (currentChar == '/') { 132 | // comment end! 133 | originalStringCurrent++; 134 | break; 135 | } 136 | } 137 | } else { 138 | // skip 139 | skipUTF8Character(&originalStringCurrent); 140 | } 141 | } 142 | } else { 143 | // nope, no comment, just copy the character 144 | *modifiedStringCurrent++ = currentChar; 145 | originalStringCurrent++; 146 | } 147 | } else { 148 | // copy character as is 149 | copyUTF8CharacterAndAdvancePointers(&originalStringCurrent, &modifiedStringCurrent); 150 | } 151 | } 152 | 153 | NSUInteger modifiedStringLength = modifiedStringCurrent - modifiedString; 154 | 155 | if (modifiedStringLength != length) { 156 | modifiedString = realloc(modifiedString, sizeof(UTF8Char) * modifiedStringLength); 157 | return [NSData dataWithBytesNoCopy:modifiedString 158 | length:modifiedStringLength 159 | freeWhenDone:YES]; 160 | } else { 161 | free(modifiedString); 162 | return data; 163 | } 164 | } 165 | 166 | 167 | + (id)JSONObjectWithCommentedUTF8Data:(NSData *)data 168 | options:(NSJSONReadingOptions)opt 169 | error:(NSError **)error { 170 | NSData *strippedData = 171 | [self dataByStrippingJSONCommentsAndWhiteSpaceOfUTF8Data:data skipBytes:0]; 172 | 173 | // NSLog(@"before:\n%@", [[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] 174 | // autorelease]); 175 | // NSLog(@"after:\n%@", [[[NSString alloc] initWithData:strippedData 176 | // encoding:NSUTF8StringEncoding] autorelease]); 177 | 178 | return [self JSONObjectWithData:strippedData options:opt error:error]; 179 | } 180 | 181 | 182 | + (NSStringEncoding)stringEncodingFromData:(NSData *)data detectedBOMSize:(NSUInteger *)bomSize { 183 | NSStringEncoding encoding = 0; 184 | if (bomSize) { 185 | *bomSize = 0; 186 | } 187 | 188 | NSUInteger fileSize = [data length]; 189 | 190 | // try to get from BOM 191 | if (fileSize >= 2) { 192 | UInt8 *bomBuffer = (UInt8 *)[data bytes]; 193 | 194 | // go back to start of file 195 | if (fileSize >= 2 && fileSize % 2 == 0) { 196 | // even amount of bytes? could be UTF-16 or UTF-32 197 | if (bomBuffer[0] == 0xFE && bomBuffer[1] == 0xFF) { 198 | // Big Endian 199 | encoding = NSUTF16StringEncoding; 200 | if (bomSize) { 201 | *bomSize = 2; 202 | } 203 | } else if (bomBuffer[0] == 0xFF && bomBuffer[1] == 0xFE) { 204 | // Little Endian 205 | encoding = NSUTF16StringEncoding; 206 | if (bomSize) { 207 | *bomSize = 2; 208 | } 209 | } else if (fileSize >= 4) { 210 | if (bomBuffer[0] == 0x00 && bomBuffer[1] == 0x00 && bomBuffer[2] == 0xFE 211 | && bomBuffer[3] == 0xFF) { 212 | // Big Endian 213 | encoding = NSUTF32StringEncoding; 214 | if (bomSize) { 215 | *bomSize = 4; 216 | } 217 | } else if (bomBuffer[0] == 0xFF && bomBuffer[1] == 0xFE && bomBuffer[2] == 0x00 218 | && bomBuffer[3] == 0x00) { 219 | // Little Endian 220 | encoding = NSUTF32StringEncoding; 221 | if (bomSize) { 222 | *bomSize = 4; 223 | } 224 | } 225 | } 226 | } 227 | 228 | if (!encoding) { 229 | if (fileSize >= 3) { 230 | if (bomBuffer[0] == 0xEF && bomBuffer[1] == 0xBB && bomBuffer[2] == 0xBF) { 231 | encoding = NSUTF8StringEncoding; 232 | if (bomSize) { 233 | *bomSize = 3; 234 | } 235 | } 236 | } 237 | } 238 | } 239 | 240 | return encoding; 241 | } 242 | 243 | + (id)JSONObjectWithCommentedData:(NSData *)data 244 | options:(NSJSONReadingOptions)opt 245 | error:(NSError **)error { 246 | if (data) { 247 | NSUInteger bomSize = 0; 248 | NSStringEncoding encoding = [self stringEncodingFromData:data detectedBOMSize:&bomSize]; 249 | 250 | if (encoding == 0 || // assume UTF-8 if no BOM is detected 251 | encoding == NSUTF8StringEncoding) { 252 | // we can use the data as is, because it is already UTF-8 253 | } else { 254 | // convert to UTF-8 first 255 | NSString *string = [[NSString alloc] initWithData:data encoding:encoding]; 256 | if (!string) { 257 | if (error) { 258 | // use the same error description, domain, and code as NSJSONSerialization 259 | *error = 260 | [NSError errorWithDomain:NSCocoaErrorDomain 261 | code:0xf00 262 | userInfo:@{ 263 | (NSString *)kCFErrorDescriptionKey : 264 | @"Unable to convert data to a string using the " 265 | @"detected encoding. The data may be corrupt." 266 | }]; 267 | } 268 | return nil; 269 | } else { 270 | data = [string dataUsingEncoding:NSUTF8StringEncoding]; 271 | [string release]; 272 | bomSize = 0; 273 | } 274 | } 275 | 276 | return [self JSONObjectWithCommentedUTF8Data:data options:opt error:error]; 277 | } else { 278 | return nil; 279 | } 280 | } 281 | 282 | 283 | + (id)JSONObjectWithCommentedContentsOfURL:(NSURL *)url 284 | options:(NSJSONReadingOptions)opt 285 | error:(NSError **)error { 286 | // load data from URL 287 | NSData *data = [NSData dataWithContentsOfURL:url options:NSDataReadingUncached error:error]; 288 | return [self JSONObjectWithCommentedData:data options:opt error:error]; 289 | } 290 | 291 | + (id)JSONObjectWithCommentedContentsOfFile:(NSString *)path 292 | options:(NSJSONReadingOptions)opt 293 | error:(NSError **)error { 294 | // load data from file 295 | NSData *data = [NSData dataWithContentsOfFile:path options:NSDataReadingUncached error:error]; 296 | 297 | return [self JSONObjectWithCommentedData:data options:opt error:error]; 298 | } 299 | 300 | + (id)JSONObjectWithCommentedString:(NSString *)string 301 | options:(NSJSONReadingOptions)opt 302 | error:(NSError **)error { 303 | NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding]; 304 | return [self JSONObjectWithCommentedUTF8Data:data options:opt error:error]; 305 | } 306 | 307 | + (NSString *)stringWithJSONObject:(id)obj 308 | options:(NSJSONWritingOptions)opt 309 | error:(NSError **)error { 310 | NSData *data = [NSJSONSerialization dataWithJSONObject:obj options:opt error:error]; 311 | if (data) { 312 | NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 313 | 314 | return [string autorelease]; 315 | } else { 316 | return nil; 317 | } 318 | } 319 | 320 | @end 321 | --------------------------------------------------------------------------------