|
| 1 | +import gleam/int |
1 | 2 | import gleam/list
|
| 3 | +import gleam/order.{Eq, Gt, Lt} |
2 | 4 | import gleam/string
|
3 | 5 |
|
4 | 6 | /// A document that can be pretty printed with `to_string`.
|
@@ -621,6 +623,144 @@ fn do_to_string(
|
621 | 623 | }
|
622 | 624 | }
|
623 | 625 |
|
| 626 | +// --- DEBUGGING UTILITIES ----------------------------------------------------- |
| 627 | + |
| 628 | +const debug_nesting = 2 |
| 629 | + |
| 630 | +/// Returns a debug version of the given document that can be pretty printed |
| 631 | +/// to see the structure of a document. |
| 632 | +/// |
| 633 | +/// This can help you see how your data structures get turned into documents |
| 634 | +/// and check if the document is what you'd expect. |
| 635 | +/// |
| 636 | +/// - `group`s are surrounded by square brackets. |
| 637 | +/// - `nest`s are surrounded by angle brackets and have a smal superscript |
| 638 | +/// with the nesting. |
| 639 | +/// - `concat`enated documents are separated by dots. |
| 640 | +/// - `break`s are rendered surrounded by curly brackets and show both the |
| 641 | +/// broken and unbroken versions. |
| 642 | +/// - `line`s are rendered as the string `lf` followed by a superscript |
| 643 | +/// number of lines. |
| 644 | +/// |
| 645 | +pub fn debug(document: Document) -> Document { |
| 646 | + case document { |
| 647 | + Text(text, _length) -> { |
| 648 | + let escaped = string.replace(each: "\"", with: "\\\"", in: text) |
| 649 | + from_string("\"" <> escaped <> "\"") |
| 650 | + } |
| 651 | + |
| 652 | + ForceBreak(doc) -> parenthesise(debug(doc), "force(", ")") |
| 653 | + |
| 654 | + Group(doc) -> |
| 655 | + parenthesise(debug(doc), "[", "]") |
| 656 | + |> force_break |
| 657 | + |
| 658 | + Nest(doc, indentation) -> |
| 659 | + parenthesise(debug(doc), superscript_number(indentation) <> "⟨", "⟩") |
| 660 | + |> force_break |
| 661 | + |
| 662 | + Break(" ", "") -> from_string("space") |
| 663 | + Break(unbroken, broken) -> |
| 664 | + from_string("{ \"" <> unbroken <> "\", \"" <> broken <> "\" }") |
| 665 | + |
| 666 | + FlexBreak(" ", "") -> from_string("flex_space") |
| 667 | + FlexBreak(unbroken, broken) -> |
| 668 | + from_string("flex{ \"" <> unbroken <> "\", \"" <> broken <> "\" }") |
| 669 | + |
| 670 | + Line(size) -> |
| 671 | + case int.compare(size, 1) { |
| 672 | + Lt | Eq -> from_string("lf") |
| 673 | + Gt -> from_string("lf" <> superscript_number(size)) |
| 674 | + } |
| 675 | + |
| 676 | + Concat(docs) -> |
| 677 | + split_groups(flatten(docs)) |
| 678 | + |> list.map(fn(docs) { |
| 679 | + case docs { |
| 680 | + [] -> panic as "empty" |
| 681 | + _ -> Nil |
| 682 | + } |
| 683 | + list.map(docs, debug) |
| 684 | + |> join(with: flex_break(" . ", " .")) |
| 685 | + |> group |
| 686 | + }) |
| 687 | + |> join(with: concat([flex_break(" . ", " ."), line])) |
| 688 | + } |
| 689 | +} |
| 690 | + |
| 691 | +fn parenthesise(document: Document, open: String, close: String) -> Document { |
| 692 | + [ |
| 693 | + from_string(open), |
| 694 | + nest(line, by: debug_nesting), |
| 695 | + nest(document, by: debug_nesting), |
| 696 | + line, |
| 697 | + from_string(close), |
| 698 | + ] |
| 699 | + |> concat |
| 700 | + |> group |
| 701 | +} |
| 702 | + |
| 703 | +fn flatten(docs: List(Document)) -> List(Document) { |
| 704 | + do_flatten(docs, []) |
| 705 | +} |
| 706 | + |
| 707 | +fn do_flatten(docs: List(Document), acc: List(Document)) -> List(Document) { |
| 708 | + case docs { |
| 709 | + [] -> list.reverse(acc) |
| 710 | + [one] -> list.reverse([one, ..acc]) |
| 711 | + [Concat(one), Concat(two), ..rest] -> |
| 712 | + do_flatten([Concat(list.append(one, two)), ..rest], acc) |
| 713 | + [Text(one, len_one), Text(two, len_two), ..rest] -> |
| 714 | + do_flatten([Text(one <> two, len_one + len_two), ..rest], acc) |
| 715 | + [one, two, ..rest] -> do_flatten([two, ..rest], [one, ..acc]) |
| 716 | + } |
| 717 | +} |
| 718 | + |
| 719 | +fn split_groups(docs: List(Document)) -> List(List(Document)) { |
| 720 | + do_split_groups(docs, [], []) |
| 721 | +} |
| 722 | + |
| 723 | +fn do_split_groups( |
| 724 | + docs: List(Document), |
| 725 | + current_group: List(Document), |
| 726 | + acc: List(List(Document)), |
| 727 | +) -> List(List(Document)) { |
| 728 | + case docs { |
| 729 | + [] -> |
| 730 | + case current_group { |
| 731 | + [] -> list.reverse(acc) |
| 732 | + _ -> list.reverse([list.reverse(current_group), ..acc]) |
| 733 | + } |
| 734 | + |
| 735 | + [Group(_) as doc, ..rest] -> |
| 736 | + case current_group { |
| 737 | + [] -> do_split_groups(rest, [], [[doc], ..acc]) |
| 738 | + _ -> |
| 739 | + do_split_groups(rest, [], [[doc], list.reverse(current_group), ..acc]) |
| 740 | + } |
| 741 | + [doc, ..rest] -> do_split_groups(rest, [doc, ..current_group], acc) |
| 742 | + } |
| 743 | +} |
| 744 | + |
| 745 | +fn superscript_number(number: Int) -> String { |
| 746 | + let assert Ok(digits) = int.digits(number, 10) |
| 747 | + use acc, digit <- list.fold(over: digits, from: "") |
| 748 | + let digit = case digit { |
| 749 | + 0 -> "⁰" |
| 750 | + 1 -> "¹" |
| 751 | + 2 -> "²" |
| 752 | + 3 -> "³" |
| 753 | + 4 -> "⁴" |
| 754 | + 5 -> "⁵" |
| 755 | + 6 -> "⁶" |
| 756 | + 7 -> "⁷" |
| 757 | + 8 -> "⁸" |
| 758 | + 9 -> "⁹" |
| 759 | + _ -> panic as "not a digit" |
| 760 | + } |
| 761 | + acc <> digit |
| 762 | +} |
| 763 | + |
624 | 764 | // --- UTILITY FUNCTIONS -------------------------------------------------------
|
625 | 765 |
|
626 | 766 | fn indentation(size: Int) -> String {
|
|
0 commit comments