| // Copyright 2017 The Wuffs Authors. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // https://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package render |
| |
| // TODO: render an *ast.Node instead of a []token.Token, as this will better |
| // allow automated refactoring tools, not just automated formatting. For now, |
| // rendering tokens is easier, especially with respect to tracking comments. |
| |
| import ( |
| "errors" |
| "io" |
| |
| t "github.com/google/wuffs/lang/token" |
| ) |
| |
| var newLine = []byte{'\n'} |
| |
| func max(a, b int) int { |
| if a > b { |
| return a |
| } |
| return b |
| } |
| |
| func Render(w io.Writer, tm *t.Map, src []t.Token, comments []string) (err error) { |
| if len(src) == 0 { |
| return nil |
| } |
| |
| const maxIndent = 0xFFFF |
| indent := 0 |
| buf := make([]byte, 0, 1024) |
| commentLine := uint32(0) |
| |
| inStruct := false |
| varNameLength := uint32(0) |
| |
| prevLine := src[0].Line - 1 |
| prevLineHanging := false |
| |
| for len(src) > 0 { |
| // Find the tokens in this line. |
| line := src[0].Line |
| i := 1 |
| for ; i < len(src) && src[i].Line == line; i++ { |
| if i == len(src) || src[i].Line != line { |
| break |
| } |
| } |
| lineTokens := src[:i] |
| src = src[i:] |
| |
| // Print any previous comments. |
| commentIndent := indent |
| if prevLineHanging { |
| commentIndent++ |
| } |
| for ; commentLine < line; commentLine++ { |
| buf = buf[:0] |
| buf = appendComment(buf, comments, commentLine, commentIndent, true) |
| if len(buf) == 0 { |
| continue |
| } |
| if commentLine > prevLine+1 { |
| if _, err = w.Write(newLine); err != nil { |
| return err |
| } |
| } |
| buf = append(buf, '\n') |
| if _, err = w.Write(buf); err != nil { |
| return err |
| } |
| varNameLength = 0 |
| prevLine = commentLine |
| } |
| |
| // Strip any trailing semi-colons. |
| hanging := prevLineHanging |
| prevLineHanging = true |
| for len(lineTokens) > 0 && lineTokens[len(lineTokens)-1].ID == t.IDSemicolon { |
| prevLineHanging = false |
| lineTokens = lineTokens[:len(lineTokens)-1] |
| } |
| if len(lineTokens) == 0 { |
| continue |
| } |
| |
| // Collapse one or more blank lines to just one. |
| if prevLine < line-1 { |
| if _, err = w.Write(newLine); err != nil { |
| return err |
| } |
| varNameLength = 0 |
| } |
| |
| // Render any leading indentation. If this line starts with a close |
| // token, outdent it so that it hopefully aligns vertically with the |
| // line containing the matching open token. |
| buf = buf[:0] |
| indentAdjustment := 0 |
| if id := lineTokens[0].ID; id == t.IDCloseDoubleCurly { |
| // No-op. |
| } else if id.IsClose() { |
| indentAdjustment-- |
| } else if hanging && ((id != t.IDOpenCurly) && (id != t.IDOpenDoubleCurly)) { |
| indentAdjustment++ |
| } |
| buf = appendTabs(buf, indent+indentAdjustment) |
| |
| // Apply or update varNameLength. |
| if len(lineTokens) < 3 { |
| varNameLength = 0 |
| } else { |
| id0 := lineTokens[0].ID |
| id1 := lineTokens[1].ID |
| if (id0 == t.IDPri) || (id0 == t.IDPub) { |
| inStruct = id1 == t.IDStruct |
| if id1 != t.IDConst { |
| varNameLength = 0 |
| } |
| } |
| if (id1 == t.IDConst) || (id0 == t.IDVar) || inStruct { |
| if varNameLength == 0 { |
| varNameLength = measureVarNameLength(tm, lineTokens, src) |
| } |
| name := "" |
| if colon := findColon(lineTokens); colon >= 0 { |
| for _, lt := range lineTokens[:colon] { |
| name = tm.ByID(lt.ID) |
| buf = append(buf, name...) |
| buf = append(buf, ' ') |
| } |
| for i := uint32(len(name)); i < varNameLength; i++ { |
| buf = append(buf, ' ') |
| } |
| lineTokens = lineTokens[colon:] |
| } |
| } else { |
| varNameLength = 0 |
| } |
| } |
| |
| // Render the lineTokens. |
| prevID, prevIsTightRight := t.ID(0), false |
| for _, tok := range lineTokens { |
| if prevID == t.IDEq || (prevID != 0 && !prevIsTightRight && !tok.ID.IsTightLeft()) { |
| // The "(" token's tight-left-ness is context dependent. For |
| // "f(x)", the "(" is tight-left. For "a * (b + c)", it is not. |
| if tok.ID != t.IDOpenParen || !isCloseIdentStrLiteralQuestion(tm, prevID) { |
| buf = append(buf, ' ') |
| } |
| } |
| |
| if s := tm.ByID(tok.ID); (s == "") || (s[0] < '0') || ('9' < s[0]) { |
| buf = append(buf, s...) |
| } else { |
| buf = appendNum(buf, s) |
| } |
| |
| if tok.ID == t.IDOpenCurly { |
| if indent == maxIndent { |
| return errors.New("render: too many \"{\" tokens") |
| } |
| indent++ |
| } else if tok.ID == t.IDCloseCurly { |
| if indent == 0 { |
| return errors.New("render: too many \"}\" tokens") |
| } |
| indent-- |
| } |
| |
| prevIsTightRight = tok.ID.IsTightRight() |
| // The "+" and "-" tokens' tight-right-ness is context dependent. |
| // The unary flavor is tight-right, the binary flavor is not. |
| if prevID != 0 && tok.ID.IsUnaryOp() && tok.ID.IsBinaryOp() { |
| // Token-based (not ast.Node-based) heuristic for whether the |
| // operator looks unary instead of binary. |
| prevIsTightRight = !isCloseIdentLiteral(tm, prevID) |
| } |
| |
| prevID = tok.ID |
| } |
| |
| buf = appendComment(buf, comments, line, 0, false) |
| buf = append(buf, '\n') |
| if _, err = w.Write(buf); err != nil { |
| return err |
| } |
| commentLine = line + 1 |
| prevLine = line |
| lastID := lineTokens[len(lineTokens)-1].ID |
| prevLineHanging = prevLineHanging && |
| (lastID != t.IDOpenCurly) && (lastID != t.IDOpenDoubleCurly) |
| } |
| |
| // Print any trailing comments. |
| for ; uint(commentLine) < uint(len(comments)); commentLine++ { |
| buf = buf[:0] |
| buf = appendComment(buf, comments, commentLine, indent, true) |
| if len(buf) > 0 { |
| if commentLine > prevLine+1 { |
| if _, err = w.Write(newLine); err != nil { |
| return err |
| } |
| } |
| buf = append(buf, '\n') |
| if _, err = w.Write(buf); err != nil { |
| return err |
| } |
| prevLine = commentLine |
| } |
| } |
| |
| return nil |
| } |
| |
| func appendComment(buf []byte, comments []string, line uint32, indent int, otherwiseEmpty bool) []byte { |
| if uint(line) < uint(len(comments)) { |
| if com := comments[line]; com != "" { |
| // Strip trailing spaces. |
| for len(com) > 0 { |
| if com[len(com)-1] != ' ' { |
| break |
| } |
| com = com[:len(com)-1] |
| } |
| |
| if otherwiseEmpty { |
| buf = appendTabs(buf, indent) |
| } else { |
| buf = append(buf, " "...) |
| } |
| buf = append(buf, com...) |
| } |
| } |
| return buf |
| } |
| |
| func appendNum(buf []byte, s string) []byte { |
| groupLen := uint32(6) |
| if (len(s) >= 2) && (s[0] == '0') { |
| if (s[1] == 'X') || (s[1] == 'x') { |
| buf = append(buf, "0x"...) |
| s = s[2:] |
| groupLen = 4 |
| } else if (s[1] == 'B') || (s[1] == 'b') { |
| buf = append(buf, "0b"...) |
| s = s[2:] |
| groupLen = 4 |
| } |
| } |
| |
| nonUnderscores := uint32(0) |
| for i := 0; i < len(s); i++ { |
| c := s[i] |
| if c == '_' { |
| continue |
| } |
| nonUnderscores++ |
| } |
| digitsUntilGroup := nonUnderscores % groupLen |
| if digitsUntilGroup == 0 { |
| digitsUntilGroup = groupLen |
| } |
| |
| for i := 0; i < len(s); i++ { |
| c := s[i] |
| if c == '_' { |
| continue |
| } else if 'a' <= c { |
| c -= 'a' - 'A' |
| } |
| |
| if digitsUntilGroup > 0 { |
| digitsUntilGroup-- |
| } else { |
| digitsUntilGroup = groupLen - 1 |
| buf = append(buf, '_') |
| } |
| buf = append(buf, c) |
| } |
| return buf |
| } |
| |
| func appendTabs(buf []byte, n int) []byte { |
| if n > 0 { |
| const tabs = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t" |
| for ; n > len(tabs); n -= len(tabs) { |
| buf = append(buf, tabs...) |
| } |
| buf = append(buf, tabs[:n]...) |
| } |
| return buf |
| } |
| |
| func isCloseIdentLiteral(tm *t.Map, x t.ID) bool { |
| return x.IsClose() || x.IsIdent(tm) || x.IsLiteral(tm) |
| } |
| |
| func isCloseIdentStrLiteralQuestion(tm *t.Map, x t.ID) bool { |
| return x.IsClose() || x.IsIdent(tm) || x.IsDQStrLiteral(tm) || |
| x.IsSQStrLiteral(tm) || (x == t.IDQuestion) |
| } |
| |
| func findColon(lineTokens []t.Token) int { |
| for i, lt := range lineTokens { |
| if lt.ID == t.IDColon { |
| return i |
| } |
| } |
| return -1 |
| } |
| |
| func measureVarNameLength(tm *t.Map, lineTokens []t.Token, remaining []t.Token) uint32 { |
| x := findColon(lineTokens) |
| if x <= 0 { |
| return 0 |
| } |
| |
| line := lineTokens[0].Line |
| length := len(tm.ByID(lineTokens[x-1].ID)) |
| for (len(remaining) > x) && |
| (remaining[0].Line == line+1) && |
| (remaining[x].Line == line+1) && |
| (remaining[x].ID == t.IDColon) { |
| |
| line = remaining[0].Line |
| length = max(length, len(tm.ByID(remaining[x-1].ID))) |
| |
| remaining = remaining[x+1:] |
| for len(remaining) > 0 && remaining[0].Line == line { |
| remaining = remaining[1:] |
| } |
| } |
| return uint32(length) |
| } |