| ... | ... | @@ -2357,10 +2357,38 @@ pub const StringifyOptions = struct { |
| 2357 | 2357 | /// Controls the whitespace emitted |
| 2358 | 2358 | whitespace: ?Whitespace = null, |
| 2359 | 2359 | |
| 2360 | | // TODO: make escaping '/' in strings optional? |
| 2360 | /// Should '/' be escaped in strings? |
| 2361 | escape_solidus: bool = false, |
| 2362 | |
| 2363 | /// Should unicode characters be escaped in strings? |
| 2364 | escape_unicode: bool = false, |
| 2365 | |
| 2361 | 2366 | // TODO: allow picking if []u8 is string or array? |
| 2362 | 2367 | }; |
| 2363 | 2368 | |
| 2369 | fn outputUnicodeEscape( |
| 2370 | codepoint: u21, |
| 2371 | out_stream: var, |
| 2372 | ) !void { |
| 2373 | if (codepoint <= 0xFFFF) { |
| 2374 | // If the character is in the Basic Multilingual Plane (U+0000 through U+FFFF), |
| 2375 | // then it may be represented as a six-character sequence: a reverse solidus, followed |
| 2376 | // by the lowercase letter u, followed by four hexadecimal digits that encode the character's code point. |
| 2377 | try out_stream.writeAll("\\u"); |
| 2378 | try std.fmt.formatIntValue(codepoint, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream); |
| 2379 | } else { |
| 2380 | assert(codepoint <= 0x10FFFF); |
| 2381 | // To escape an extended character that is not in the Basic Multilingual Plane, |
| 2382 | // the character is represented as a 12-character sequence, encoding the UTF-16 surrogate pair. |
| 2383 | const high = @intCast(u16, (codepoint - 0x10000) >> 10) + 0xD800; |
| 2384 | const low = @intCast(u16, codepoint & 0x3FF) + 0xDC00; |
| 2385 | try out_stream.writeAll("\\u"); |
| 2386 | try std.fmt.formatIntValue(high, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream); |
| 2387 | try out_stream.writeAll("\\u"); |
| 2388 | try std.fmt.formatIntValue(low, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream); |
| 2389 | } |
| 2390 | } |
| 2391 | |
| 2364 | 2392 | pub fn stringify( |
| 2365 | 2393 | value: var, |
| 2366 | 2394 | options: StringifyOptions, |
| ... | ... | @@ -2467,12 +2495,21 @@ pub fn stringify( |
| 2467 | 2495 | var i: usize = 0; |
| 2468 | 2496 | while (i < value.len) : (i += 1) { |
| 2469 | 2497 | switch (value[i]) { |
| 2470 | | // normal ascii characters |
| 2471 | | 0x20...0x21, 0x23...0x2E, 0x30...0x5B, 0x5D...0x7F => try out_stream.writeAll(value[i .. i + 1]), |
| 2472 | | // control characters with short escapes |
| 2498 | // normal ascii character |
| 2499 | 0x20...0x21, 0x23...0x2E, 0x30...0x5B, 0x5D...0x7F => |c| try out_stream.writeByte(c), |
| 2500 | // only 2 characters that *must* be escaped |
| 2473 | 2501 | '\\' => try out_stream.writeAll("\\\\"), |
| 2474 | 2502 | '\"' => try out_stream.writeAll("\\\""), |
| 2475 | | '/' => try out_stream.writeAll("\\/"), |
| 2503 | // solidus is optional to escape |
| 2504 | '/' => { |
| 2505 | if (options.escape_solidus) { |
| 2506 | try out_stream.writeAll("\\/"); |
| 2507 | } else { |
| 2508 | try out_stream.writeByte('\\'); |
| 2509 | } |
| 2510 | }, |
| 2511 | // control characters with short escapes |
| 2512 | // TODO: option to switch between unicode and 'short' forms? |
| 2476 | 2513 | 0x8 => try out_stream.writeAll("\\b"), |
| 2477 | 2514 | 0xC => try out_stream.writeAll("\\f"), |
| 2478 | 2515 | '\n' => try out_stream.writeAll("\\n"), |
| ... | ... | @@ -2480,22 +2517,12 @@ pub fn stringify( |
| 2480 | 2517 | '\t' => try out_stream.writeAll("\\t"), |
| 2481 | 2518 | else => { |
| 2482 | 2519 | const ulen = std.unicode.utf8ByteSequenceLength(value[i]) catch unreachable; |
| 2483 | | const codepoint = std.unicode.utf8Decode(value[i .. i + ulen]) catch unreachable; |
| 2484 | | if (codepoint <= 0xFFFF) { |
| 2485 | | // If the character is in the Basic Multilingual Plane (U+0000 through U+FFFF), |
| 2486 | | // then it may be represented as a six-character sequence: a reverse solidus, followed |
| 2487 | | // by the lowercase letter u, followed by four hexadecimal digits that encode the character's code point. |
| 2488 | | try out_stream.writeAll("\\u"); |
| 2489 | | try std.fmt.formatIntValue(codepoint, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream); |
| 2520 | // control characters (only things left with 1 byte length) should always be printed as unicode escapes |
| 2521 | if (ulen == 1 or options.escape_unicode) { |
| 2522 | const codepoint = std.unicode.utf8Decode(value[i .. i + ulen]) catch unreachable; |
| 2523 | try outputUnicodeEscape(codepoint, out_stream); |
| 2490 | 2524 | } else { |
| 2491 | | // To escape an extended character that is not in the Basic Multilingual Plane, |
| 2492 | | // the character is represented as a 12-character sequence, encoding the UTF-16 surrogate pair. |
| 2493 | | const high = @intCast(u16, (codepoint - 0x10000) >> 10) + 0xD800; |
| 2494 | | const low = @intCast(u16, codepoint & 0x3FF) + 0xDC00; |
| 2495 | | try out_stream.writeAll("\\u"); |
| 2496 | | try std.fmt.formatIntValue(high, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream); |
| 2497 | | try out_stream.writeAll("\\u"); |
| 2498 | | try std.fmt.formatIntValue(low, "x", std.fmt.FormatOptions{ .width = 4, .fill = '0' }, out_stream); |
| 2525 | try out_stream.writeAll(value[i .. i + ulen]); |
| 2499 | 2526 | } |
| 2500 | 2527 | i += ulen - 1; |
| 2501 | 2528 | }, |
| ... | ... | @@ -2609,15 +2636,25 @@ test "stringify basic types" { |
| 2609 | 2636 | test "stringify string" { |
| 2610 | 2637 | try teststringify("\"hello\"", "hello", StringifyOptions{}); |
| 2611 | 2638 | try teststringify("\"with\\nescapes\\r\"", "with\nescapes\r", StringifyOptions{}); |
| 2639 | try teststringify("\"with\\nescapes\\r\"", "with\nescapes\r", StringifyOptions{ .escape_unicode = true }); |
| 2612 | 2640 | try teststringify("\"with unicode\\u0001\"", "with unicode\u{1}", StringifyOptions{}); |
| 2613 | | try teststringify("\"with unicode\\u0080\"", "with unicode\u{80}", StringifyOptions{}); |
| 2614 | | try teststringify("\"with unicode\\u00ff\"", "with unicode\u{FF}", StringifyOptions{}); |
| 2615 | | try teststringify("\"with unicode\\u0100\"", "with unicode\u{100}", StringifyOptions{}); |
| 2616 | | try teststringify("\"with unicode\\u0800\"", "with unicode\u{800}", StringifyOptions{}); |
| 2617 | | try teststringify("\"with unicode\\u8000\"", "with unicode\u{8000}", StringifyOptions{}); |
| 2618 | | try teststringify("\"with unicode\\ud799\"", "with unicode\u{D799}", StringifyOptions{}); |
| 2619 | | try teststringify("\"with unicode\\ud800\\udc00\"", "with unicode\u{10000}", StringifyOptions{}); |
| 2620 | | try teststringify("\"with unicode\\udbff\\udfff\"", "with unicode\u{10FFFF}", StringifyOptions{}); |
| 2641 | try teststringify("\"with unicode\\u0001\"", "with unicode\u{1}", StringifyOptions{ .escape_unicode = true }); |
| 2642 | try teststringify("\"with unicode\u{80}\"", "with unicode\u{80}", StringifyOptions{}); |
| 2643 | try teststringify("\"with unicode\\u0080\"", "with unicode\u{80}", StringifyOptions{ .escape_unicode = true }); |
| 2644 | try teststringify("\"with unicode\u{FF}\"", "with unicode\u{FF}", StringifyOptions{}); |
| 2645 | try teststringify("\"with unicode\\u00ff\"", "with unicode\u{FF}", StringifyOptions{ .escape_unicode = true }); |
| 2646 | try teststringify("\"with unicode\u{100}\"", "with unicode\u{100}", StringifyOptions{}); |
| 2647 | try teststringify("\"with unicode\\u0100\"", "with unicode\u{100}", StringifyOptions{ .escape_unicode = true }); |
| 2648 | try teststringify("\"with unicode\u{800}\"", "with unicode\u{800}", StringifyOptions{}); |
| 2649 | try teststringify("\"with unicode\\u0800\"", "with unicode\u{800}", StringifyOptions{ .escape_unicode = true }); |
| 2650 | try teststringify("\"with unicode\u{8000}\"", "with unicode\u{8000}", StringifyOptions{}); |
| 2651 | try teststringify("\"with unicode\\u8000\"", "with unicode\u{8000}", StringifyOptions{ .escape_unicode = true }); |
| 2652 | try teststringify("\"with unicode\u{D799}\"", "with unicode\u{D799}", StringifyOptions{}); |
| 2653 | try teststringify("\"with unicode\\ud799\"", "with unicode\u{D799}", StringifyOptions{ .escape_unicode = true }); |
| 2654 | try teststringify("\"with unicode\u{10000}\"", "with unicode\u{10000}", StringifyOptions{}); |
| 2655 | try teststringify("\"with unicode\\ud800\\udc00\"", "with unicode\u{10000}", StringifyOptions{ .escape_unicode = true }); |
| 2656 | try teststringify("\"with unicode\u{10FFFF}\"", "with unicode\u{10FFFF}", StringifyOptions{}); |
| 2657 | try teststringify("\"with unicode\\udbff\\udfff\"", "with unicode\u{10FFFF}", StringifyOptions{ .escape_unicode = true }); |
| 2621 | 2658 | } |
| 2622 | 2659 | |
| 2623 | 2660 | test "stringify tagged unions" { |