|
| 1 | +package testutil |
| 2 | + |
| 3 | +import ( |
| 4 | + "fmt" |
| 5 | +) |
| 6 | + |
| 7 | +// Undent removes leading indentation/white-space from given string and returns |
| 8 | +// it as a string. Useful for inlining YAML manifests in Go code. Inline YAML |
| 9 | +// manifests in the Go test files makes it easier to read the test case as |
| 10 | +// opposed to reading verbose-y Go structs. |
| 11 | +// |
| 12 | +// This was copied from https://github.com/jimeh/Undent/blob/main/Undent.go, all |
| 13 | +// credit goes to the author, Jim Myhrberg. |
| 14 | +// |
| 15 | +// For code readability purposes, it is possible to start the literal string |
| 16 | +// with "\n", in which case, the first line is ignored. For example, in the |
| 17 | +// following example, name and labels have the same indentation level but aren't |
| 18 | +// aligned due to the leading '`': |
| 19 | +// |
| 20 | +// Undent( |
| 21 | +// ` name: foo |
| 22 | +// labels: |
| 23 | +// foo: bar`) |
| 24 | +// |
| 25 | +// Instead, you can write a well-aligned text like this: |
| 26 | +// |
| 27 | +// Undent(` |
| 28 | +// name: foo |
| 29 | +// labels: |
| 30 | +// foo: bar`) |
| 31 | +// |
| 32 | +// For code readability purposes, it is also possible to not have the correct |
| 33 | +// number of indentations in the last line. For example: |
| 34 | +// |
| 35 | +// Undent(` |
| 36 | +// foo |
| 37 | +// bar |
| 38 | +// `) |
| 39 | +// |
| 40 | +// For code readability purposes, you can also omit the indentations for empty |
| 41 | +// lines. For example: |
| 42 | +// |
| 43 | +// Undent(` |
| 44 | +// foo <---- 4 spaces |
| 45 | +// <---- no indentation here |
| 46 | +// bar <---- 4 spaces |
| 47 | +// `) |
| 48 | +func Undent(s string) string { |
| 49 | + if len(s) == 0 { |
| 50 | + return "" |
| 51 | + } |
| 52 | + |
| 53 | + // indentsPerLine is the minimal indent level that we have found up to now. |
| 54 | + // For example, "\t\t" corresponds to an indentation of 2, and " " an |
| 55 | + // indentation of 3. |
| 56 | + indentsPerLine := 99999999999 |
| 57 | + indentedLinesCnt := 0 |
| 58 | + |
| 59 | + // lineOffsets tells you where the beginning of each line is in terms of |
| 60 | + // offset. Example: |
| 61 | + // "\tfoo\n\tbar\n" -> [0, 5] |
| 62 | + // 0 5 |
| 63 | + var lineOffsets []int |
| 64 | + |
| 65 | + // For code readability purposes, users can leave the first line empty. |
| 66 | + if s[0] != '\n' { |
| 67 | + lineOffsets = append(lineOffsets, 0) |
| 68 | + } |
| 69 | + |
| 70 | + curLineIndent := 0 // Number of tabs or spaces in the current line. |
| 71 | + for pos := 0; pos < len(s); pos++ { |
| 72 | + if s[pos] == '\n' { |
| 73 | + if pos+1 < len(s) { |
| 74 | + lineOffsets = append(lineOffsets, pos+1) |
| 75 | + } |
| 76 | + curLineIndent = 0 |
| 77 | + continue |
| 78 | + } |
| 79 | + |
| 80 | + // Skip to the next line if we are already beyond the minimal indent |
| 81 | + // level that we have found so far. The rest of this line will be kept |
| 82 | + // as-is. |
| 83 | + if curLineIndent >= indentsPerLine { |
| 84 | + continue |
| 85 | + } |
| 86 | + |
| 87 | + // The minimal indent level that we have found so far in previous lines |
| 88 | + // might not be the smallest indent level. Once we hit the first |
| 89 | + // non-indent char, let's check whether it is the new minimal indent |
| 90 | + // level. |
| 91 | + if s[pos] != ' ' && s[pos] != '\t' { |
| 92 | + if curLineIndent != 0 { |
| 93 | + indentedLinesCnt++ |
| 94 | + } |
| 95 | + indentsPerLine = curLineIndent |
| 96 | + continue |
| 97 | + } |
| 98 | + |
| 99 | + curLineIndent++ |
| 100 | + } |
| 101 | + |
| 102 | + // Extract each line without indentation. |
| 103 | + out := make([]byte, 0, len(s)-(indentsPerLine*indentedLinesCnt)) |
| 104 | + |
| 105 | + for line := 0; line < len(lineOffsets); line++ { |
| 106 | + first := lineOffsets[line] |
| 107 | + |
| 108 | + // Index of the last character of the line. It is often the '\n' |
| 109 | + // character, except for the last line. |
| 110 | + var last int |
| 111 | + if line == len(lineOffsets)-1 { |
| 112 | + last = len(s) - 1 |
| 113 | + } else { |
| 114 | + last = lineOffsets[line+1] - 1 |
| 115 | + } |
| 116 | + |
| 117 | + var lineStr string |
| 118 | + switch { |
| 119 | + // Case 0: if the first line is empty, let's skip it. |
| 120 | + case line == 0 && first == last: |
| 121 | + lineStr = "" |
| 122 | + |
| 123 | + // Case 1: we want the user to be able to omit some tabs or spaces in |
| 124 | + // the last line for readability purposes. |
| 125 | + case line == len(lineOffsets)-1 && s[last] != '\n' && isIndent(s[first:last]): |
| 126 | + lineStr = "" |
| 127 | + |
| 128 | + // Case 2: we want the user to be able to omit the indentations for |
| 129 | + // empty lines for readability purposes. |
| 130 | + case first == last: |
| 131 | + lineStr = "\n" |
| 132 | + |
| 133 | + // Case 3: error when a line doesn't contain the correct indentation |
| 134 | + // level. |
| 135 | + case first+indentsPerLine > last: |
| 136 | + panic(fmt.Sprintf("line %d has an incorrect indent level: %q", line, s[first:last])) |
| 137 | + |
| 138 | + // Case 4: at this point, the indent level is correct, so let's remove |
| 139 | + // the indentation and keep the rest. |
| 140 | + case first+indentsPerLine <= last: |
| 141 | + lineStr = s[first+indentsPerLine : last+1] |
| 142 | + |
| 143 | + default: |
| 144 | + panic(fmt.Sprintf("unexpected case: first: %d, last: %d, indentsPerLine: %d, line: %q", first, last, indentsPerLine, s[first:last])) |
| 145 | + } |
| 146 | + out = append(out, lineStr...) |
| 147 | + } |
| 148 | + |
| 149 | + return string(out) |
| 150 | +} |
| 151 | + |
| 152 | +// isIndent returns true if the given string is only made of spaces or a |
| 153 | +// tabs. |
| 154 | +func isIndent(s string) bool { |
| 155 | + for _, r := range s { |
| 156 | + if r != ' ' && r != '\t' { |
| 157 | + return false |
| 158 | + } |
| 159 | + } |
| 160 | + return true |
| 161 | +} |
0 commit comments