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