| ... | @@ -2357,10 +2357,38 @@ pub const StringifyOptions = struct { | ... | @@ -2357,10 +2357,38 @@ pub const StringifyOptions = struct { |
| 2357 | /// Controls the whitespace emitted | 2357 | /// Controls the whitespace emitted |
| 2358 | whitespace: ?Whitespace = null, | 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 | // TODO: allow picking if []u8 is string or array? | 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 | pub fn stringify( | 2392 | pub fn stringify( |
| 2365 | value: var, | 2393 | value: var, |
| 2366 | options: StringifyOptions, | 2394 | options: StringifyOptions, |
| ... | @@ -2467,12 +2495,21 @@ pub fn stringify( | ... | @@ -2467,12 +2495,21 @@ pub fn stringify( |
| 2467 | var i: usize = 0; | 2495 | var i: usize = 0; |
| 2468 | while (i < value.len) : (i += 1) { | 2496 | while (i < value.len) : (i += 1) { |
| 2469 | switch (value[i]) { | 2497 | switch (value[i]) { |
| 2470 | // normal ascii characters | 2498 | // normal ascii character |
| 2471 | 0x20...0x21, 0x23...0x2E, 0x30...0x5B, 0x5D...0x7F => try out_stream.writeAll(value[i .. i + 1]), | 2499 | 0x20...0x21, 0x23...0x2E, 0x30...0x5B, 0x5D...0x7F => |c| try out_stream.writeByte(c), |
| 2472 | // control characters with short escapes | 2500 | // only 2 characters that *must* be escaped |
| 2473 | '\\' => try out_stream.writeAll("\\\\"), | 2501 | '\\' => try out_stream.writeAll("\\\\"), |
| 2474 | '\"' => try out_stream.writeAll("\\\""), | 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 | 0x8 => try out_stream.writeAll("\\b"), | 2513 | 0x8 => try out_stream.writeAll("\\b"), |
| 2477 | 0xC => try out_stream.writeAll("\\f"), | 2514 | 0xC => try out_stream.writeAll("\\f"), |
| 2478 | '\n' => try out_stream.writeAll("\\n"), | 2515 | '\n' => try out_stream.writeAll("\\n"), |
| ... | @@ -2480,22 +2517,12 @@ pub fn stringify( | ... | @@ -2480,22 +2517,12 @@ pub fn stringify( |
| 2480 | '\t' => try out_stream.writeAll("\\t"), | 2517 | '\t' => try out_stream.writeAll("\\t"), |
| 2481 | else => { | 2518 | else => { |
| 2482 | const ulen = std.unicode.utf8ByteSequenceLength(value[i]) catch unreachable; | 2519 | const ulen = std.unicode.utf8ByteSequenceLength(value[i]) catch unreachable; |
| 2483 | const codepoint = std.unicode.utf8Decode(value[i .. i + ulen]) catch unreachable; | 2520 | // control characters (only things left with 1 byte length) should always be printed as unicode escapes |
| 2484 | if (codepoint <= 0xFFFF) { | 2521 | if (ulen == 1 or options.escape_unicode) { |
| 2485 | // If the character is in the Basic Multilingual Plane (U+0000 through U+FFFF), | 2522 | const codepoint = std.unicode.utf8Decode(value[i .. i + ulen]) catch unreachable; |
| 2486 | // then it may be represented as a six-character sequence: a reverse solidus, followed | 2523 | try outputUnicodeEscape(codepoint, out_stream); |
| 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); | | |
| 2490 | } else { | 2524 | } else { |
| 2491 | // To escape an extended character that is not in the Basic Multilingual Plane, | 2525 | try out_stream.writeAll(value[i .. i + ulen]); |
| 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); | | |
| 2499 | } | 2526 | } |
| 2500 | i += ulen - 1; | 2527 | i += ulen - 1; |
| 2501 | }, | 2528 | }, |
| ... | @@ -2609,15 +2636,25 @@ test "stringify basic types" { | ... | @@ -2609,15 +2636,25 @@ test "stringify basic types" { |
| 2609 | test "stringify string" { | 2636 | test "stringify string" { |
| 2610 | try teststringify("\"hello\"", "hello", StringifyOptions{}); | 2637 | try teststringify("\"hello\"", "hello", StringifyOptions{}); |
| 2611 | try teststringify("\"with\\nescapes\\r\"", "with\nescapes\r", StringifyOptions{}); | 2638 | try teststringify("\"with\\nescapes\\r\"", "with\nescapes\r", StringifyOptions{}); |
| | 2639 | try teststringify("\"with\\nescapes\\r\"", "with\nescapes\r", StringifyOptions{ .escape_unicode = true }); |
| 2612 | try teststringify("\"with unicode\\u0001\"", "with unicode\u{1}", StringifyOptions{}); | 2640 | try teststringify("\"with unicode\\u0001\"", "with unicode\u{1}", StringifyOptions{}); |
| 2613 | try teststringify("\"with unicode\\u0080\"", "with unicode\u{80}", StringifyOptions{}); | 2641 | try teststringify("\"with unicode\\u0001\"", "with unicode\u{1}", StringifyOptions{ .escape_unicode = true }); |
| 2614 | try teststringify("\"with unicode\\u00ff\"", "with unicode\u{FF}", StringifyOptions{}); | 2642 | try teststringify("\"with unicode\u{80}\"", "with unicode\u{80}", StringifyOptions{}); |
| 2615 | try teststringify("\"with unicode\\u0100\"", "with unicode\u{100}", StringifyOptions{}); | 2643 | try teststringify("\"with unicode\\u0080\"", "with unicode\u{80}", StringifyOptions{ .escape_unicode = true }); |
| 2616 | try teststringify("\"with unicode\\u0800\"", "with unicode\u{800}", StringifyOptions{}); | 2644 | try teststringify("\"with unicode\u{FF}\"", "with unicode\u{FF}", StringifyOptions{}); |
| 2617 | try teststringify("\"with unicode\\u8000\"", "with unicode\u{8000}", StringifyOptions{}); | 2645 | try teststringify("\"with unicode\\u00ff\"", "with unicode\u{FF}", StringifyOptions{ .escape_unicode = true }); |
| 2618 | try teststringify("\"with unicode\\ud799\"", "with unicode\u{D799}", StringifyOptions{}); | 2646 | try teststringify("\"with unicode\u{100}\"", "with unicode\u{100}", StringifyOptions{}); |
| 2619 | try teststringify("\"with unicode\\ud800\\udc00\"", "with unicode\u{10000}", StringifyOptions{}); | 2647 | try teststringify("\"with unicode\\u0100\"", "with unicode\u{100}", StringifyOptions{ .escape_unicode = true }); |
| 2620 | try teststringify("\"with unicode\\udbff\\udfff\"", "with unicode\u{10FFFF}", StringifyOptions{}); | 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 | test "stringify tagged unions" { | 2660 | test "stringify tagged unions" { |