align.mx raw
1 package main
2
3 func alignOutput(src string) string {
4 lines := splitLines(src)
5 alignFieldTypes(lines)
6 alignTrailingComments(lines)
7 return joinLines(lines)
8 }
9
10 func splitLines(s string) []string {
11 out := []string{:0:64}
12 start := int32(0)
13 for i := int32(0); i < int32(len(s)); i++ {
14 if s[i] == '\n' {
15 out = append(out, s[start:i])
16 start = i + 1
17 }
18 }
19 if start < int32(len(s)) {
20 out = append(out, s[start:])
21 }
22 return out
23 }
24
25 func joinLines(lines []string) string {
26 if len(lines) == 0 { return "" }
27 out := []byte{:0:1024}
28 for i := int32(0); i < int32(len(lines)); i++ {
29 out = append(out, lines[i]...)
30 out = append(out, '\n')
31 }
32 return string(out)
33 }
34
35 func lineIndent(line string) (n int32) {
36 for n < int32(len(line)) && line[n] == '\t' { n++ }
37 return
38 }
39
40 func trimLeft(s string) string {
41 i := int32(0)
42 for i < int32(len(s)) && (s[i] == ' ' || s[i] == '\t') { i++ }
43 return s[i:]
44 }
45
46 func isKeywordPrefix(s string) (yes bool) {
47 kws := [20]string{
48 "type ", "func ", "var ", "const ", "for ", "if ", "else ",
49 "switch ", "case ", "default:", "select ", "return ",
50 "defer ", "go ", "goto ", "break", "continue", "import ",
51 "package ", "//",
52 }
53 for i := int32(0); i < int32(len(kws)); i++ {
54 k := kws[i]
55 if int32(len(s)) >= int32(len(k)) && s[:len(k)] == k {
56 yes = true
57 return
58 }
59 }
60 return
61 }
62
63 func isFieldLine(line string) (yes bool) {
64 s := trimLeft(line)
65 if len(s) == 0 { return }
66 if s[0] == '/' { return }
67 if !isIdentStart(rune(s[0])) { return }
68 if isKeywordPrefix(s) { return }
69 spaceIdx := indexByte(s, ' ')
70 tabIdx := indexByte(s, '\t')
71 sepIdx := spaceIdx
72 if sepIdx < 0 || (tabIdx >= 0 && tabIdx < sepIdx) { sepIdx = tabIdx }
73 if sepIdx < 0 { return }
74 rest := trimLeft(s[sepIdx:])
75 if len(rest) == 0 { return }
76 if rest[0] == '=' || rest[0] == '+' || rest[0] == '-' || rest[0] == ':' ||
77 rest[0] == '|' || rest[0] == '(' { return }
78 yes = true
79 return
80 }
81
82 func isIdentStart(ch rune) (yes bool) {
83 if 'a' <= ch && ch <= 'z' || 'A' <= ch && ch <= 'Z' || ch == '_' {
84 yes = true
85 }
86 return
87 }
88
89 func indexByte(s string, b uint8) (idx int32) {
90 for idx = 0; idx < int32(len(s)); idx++ {
91 if s[idx] == b { return }
92 }
93 idx = -1
94 return
95 }
96
97 func fieldNameEnd(line string) (pos int32) {
98 s := trimLeft(line)
99 indent := int32(len(line)) - int32(len(s))
100 pos = indent
101 for {
102 for pos < int32(len(line)) && line[pos] != ' ' && line[pos] != '\t' && line[pos] != ',' { pos++ }
103 if pos < int32(len(line)) && line[pos] == ',' {
104 pos++
105 for pos < int32(len(line)) && line[pos] == ' ' { pos++ }
106 continue
107 }
108 break
109 }
110 return
111 }
112
113 func commentStart(line string) (pos int32) {
114 pos = -1
115 inStr := false
116 esc := false
117 for i := int32(0); i < int32(len(line)) - 1; i++ {
118 ch := line[i]
119 if esc { esc = false; continue }
120 if ch == '\\' { esc = true; continue }
121 if ch == '"' || ch == '\'' || ch == '`' { inStr = !inStr; continue }
122 if inStr { continue }
123 if ch == '/' && line[i+1] == '/' {
124 pos = i
125 return
126 }
127 }
128 return
129 }
130
131 // --- field type alignment ---
132
133 func alignFieldTypes(lines []string) {
134 i := int32(0)
135 n := int32(len(lines))
136 for i < n {
137 gStart, gEnd := findFieldGroup(lines, i, n)
138 if gEnd > gStart {
139 alignGroup(lines, gStart, gEnd)
140 i = gEnd
141 } else {
142 i++
143 }
144 }
145 }
146
147 func findFieldGroup(lines []string, from int32, n int32) (gStart int32, gEnd int32) {
148 gStart = from
149 for gStart < n && !isFieldLine(lines[gStart]) { gStart++ }
150 if gStart >= n { gEnd = gStart; return }
151 indent := lineIndent(lines[gStart])
152 gEnd = gStart + 1
153 blanks := int32(0)
154 for gEnd < n {
155 line := lines[gEnd]
156 if len(line) == 0 || len(trimLeft(line)) == 0 {
157 blanks++
158 if blanks >= 2 { break }
159 gEnd++
160 continue
161 }
162 blanks = 0
163 if lineIndent(line) != indent { break }
164 if !isFieldLine(line) { break }
165 gEnd++
166 }
167 if gEnd - gStart < 2 { gEnd = gStart }
168 return
169 }
170
171 func alignGroup(lines []string, from int32, to int32) {
172 maxNameW := int32(0)
173 for i := from; i < to; i++ {
174 line := lines[i]
175 if len(trimLeft(line)) == 0 { continue }
176 ne := fieldNameEnd(line)
177 indent := lineIndent(line)
178 nameW := ne - indent
179 if nameW > maxNameW { maxNameW = nameW }
180 }
181 for i := from; i < to; i++ {
182 line := lines[i]
183 if len(trimLeft(line)) == 0 { continue }
184 ne := fieldNameEnd(line)
185 indent := lineIndent(line)
186 nameW := ne - indent
187 pad := maxNameW - nameW
188 if pad <= 0 { continue }
189 rest := line[ne:]
190 rest = trimLeft(rest)
191 out := []byte{:0:int32(len(line)) + pad}
192 out = append(out, line[:ne]...)
193 for j := int32(0); j <= pad; j++ { out = append(out, ' ') }
194 out = append(out, rest...)
195 lines[i] = string(out)
196 }
197 }
198
199 // --- trailing comment alignment ---
200
201 func alignTrailingComments(lines []string) {
202 i := int32(0)
203 n := int32(len(lines))
204 for i < n {
205 gStart := i
206 for gStart < n {
207 if commentStart(lines[gStart]) >= 0 { break }
208 gStart++
209 }
210 if gStart >= n { break }
211 gEnd := gStart + 1
212 blanks := int32(0)
213 for gEnd < n {
214 line := lines[gEnd]
215 if len(line) == 0 || len(trimLeft(line)) == 0 {
216 blanks++
217 if blanks >= 2 { break }
218 gEnd++
219 continue
220 }
221 blanks = 0
222 if commentStart(line) < 0 { break }
223 gEnd++
224 }
225 if gEnd - gStart >= 2 {
226 alignCommentGroup(lines, gStart, gEnd)
227 }
228 i = gEnd
229 }
230 }
231
232 func alignCommentGroup(lines []string, from int32, to int32) {
233 maxCode := int32(0)
234 for i := from; i < to; i++ {
235 cs := commentStart(lines[i])
236 if cs < 0 { continue }
237 codeW := cs
238 for codeW > 0 && (lines[i][codeW-1] == ' ' || lines[i][codeW-1] == '\t') {
239 codeW--
240 }
241 if codeW > maxCode { maxCode = codeW }
242 }
243 for i := from; i < to; i++ {
244 cs := commentStart(lines[i])
245 if cs < 0 { continue }
246 codeW := cs
247 for codeW > 0 && (lines[i][codeW-1] == ' ' || lines[i][codeW-1] == '\t') {
248 codeW--
249 }
250 cmt := lines[i][cs:]
251 out := []byte{:0:maxCode + 1 + int32(len(cmt))}
252 out = append(out, lines[i][:codeW]...)
253 pad := maxCode - codeW + 1
254 for j := int32(0); j < pad; j++ { out = append(out, ' ') }
255 out = append(out, cmt...)
256 lines[i] = string(out)
257 }
258 }
259