1package main 2 3import ( 4 "errors" 5 "fmt" 6 7 "github.com/xyproto/mode" 8) 9 10// QuoteState keeps track of if we're within a multi-line comment, single quotes, double quotes or multi-line quotes. 11// Single line comments are not kept track of in the same way, they can be detected just by checking the current line. 12// If one of the ints are > 0, the other ints should not be added to. 13// MultiLine comments (/* ... */) are special. 14// This could be a flag int instead 15type QuoteState struct { 16 singleQuote int 17 doubleQuote int 18 backtick int 19 multiLineComment bool 20 singleLineComment bool 21 singleLineCommentMarker string 22 singleLineCommentMarkerRunes []rune 23 firstRuneInSingleLineCommentMarker rune 24 lastRuneInSingleLineCommentMarker rune 25 startedMultiLineString bool 26 startedMultiLineComment bool 27 stoppedMultiLineComment bool 28 containsMultiLineComments bool 29 parCount int // Parenthesis count 30 braCount int // Square bracket count 31 mode mode.Mode 32 ignoreSingleQuotes bool 33} 34 35// NewQuoteState takes a singleLineCommentMarker (such as "//" or "#") and returns a pointer to a new QuoteState struct 36func NewQuoteState(singleLineCommentMarker string, m mode.Mode, ignoreSingleQuotes bool) (*QuoteState, error) { 37 var q QuoteState 38 q.singleLineCommentMarker = singleLineCommentMarker 39 q.singleLineCommentMarkerRunes = []rune(singleLineCommentMarker) 40 lensr := len(q.singleLineCommentMarkerRunes) 41 if lensr == 0 { 42 return nil, errors.New("single line comment marker is empty") 43 } 44 q.firstRuneInSingleLineCommentMarker = q.singleLineCommentMarkerRunes[0] 45 q.lastRuneInSingleLineCommentMarker = q.singleLineCommentMarkerRunes[lensr-1] 46 q.mode = m 47 q.ignoreSingleQuotes = ignoreSingleQuotes 48 return &q, nil 49} 50 51// None returns true if we're not within ', "", `, /* ... */ or a single-line quote right now 52func (q *QuoteState) None() bool { 53 return q.singleQuote == 0 && q.doubleQuote == 0 && q.backtick == 0 && !q.multiLineComment && !q.singleLineComment 54} 55 56// OnlyBacktick returns true if we're only within a ` quote 57func (q *QuoteState) OnlyBacktick() bool { 58 return q.singleQuote == 0 && q.doubleQuote == 0 && q.backtick > 0 && !q.multiLineComment && !q.singleLineComment 59} 60 61// OnlySingleQuote returns true if we're only within a ' quote 62func (q *QuoteState) OnlySingleQuote() bool { 63 return q.singleQuote > 0 && q.doubleQuote == 0 && q.backtick == 0 && !q.multiLineComment && !q.singleLineComment 64} 65 66// OnlyDoubleQuote returns true if we're only within a " quote 67func (q *QuoteState) OnlyDoubleQuote() bool { 68 return q.singleQuote == 0 && q.doubleQuote > 0 && q.backtick == 0 && !q.multiLineComment && !q.singleLineComment 69} 70 71// OnlyMultiLineComment returns true if we're only within a multi-line comment 72func (q *QuoteState) OnlyMultiLineComment() bool { 73 return q.singleQuote == 0 && q.doubleQuote == 0 && q.backtick == 0 && q.multiLineComment && !q.singleLineComment 74} 75 76// String returns info about the current quote state 77func (q *QuoteState) String() string { 78 return fmt.Sprintf("singleQuote=%v doubleQuote=%v backtick=%v multiLineComment=%v singleLineComment=%v startedMultiLineString=%v\n", q.singleQuote, q.doubleQuote, q.backtick, q.multiLineComment, q.singleLineComment, q.startedMultiLineString) 79} 80 81// ProcessRune is for processing single runes 82func (q *QuoteState) ProcessRune(r, prevRune, prevPrevRune rune) { 83 switch r { 84 case '`': 85 if q.None() { 86 q.backtick++ 87 q.startedMultiLineString = true 88 } else { 89 q.backtick-- 90 if q.backtick < 0 { 91 q.backtick = 0 92 } 93 } 94 case '"': 95 if prevPrevRune == '"' && prevRune == '"' { 96 q.startedMultiLineString = q.None() 97 } else if prevRune != '\\' { 98 if q.None() { 99 q.doubleQuote++ 100 } else { 101 q.doubleQuote-- 102 if q.doubleQuote < 0 { 103 q.doubleQuote = 0 104 } 105 } 106 } 107 case '\'': 108 if prevRune != '\\' { 109 if q.ignoreSingleQuotes || q.mode == mode.Lisp || q.mode == mode.Clojure { 110 return 111 } 112 if q.None() { 113 q.singleQuote++ 114 } else { 115 q.singleQuote-- 116 if q.singleQuote < 0 { 117 q.singleQuote = 0 118 } 119 } 120 } 121 case '*': // support multi-line comments 122 if q.firstRuneInSingleLineCommentMarker != '#' && prevRune == '/' && (prevPrevRune == '\n' || prevPrevRune == ' ' || prevPrevRune == '\t') && q.None() { 123 // C-style 124 q.multiLineComment = true 125 q.startedMultiLineComment = true 126 } else if (q.mode == mode.StandardML || q.mode == mode.OCaml) && prevRune == '(' && q.None() { 127 // Standard ML 128 q.parCount-- // Not a parenthesis start after all, but the start of a multiline comment 129 q.multiLineComment = true 130 q.startedMultiLineComment = true 131 } 132 case '-': // support for HTML-style and XML-style multi-line comments 133 if prevRune == '!' && prevPrevRune == '<' && q.None() { 134 q.multiLineComment = true 135 q.startedMultiLineComment = true 136 } 137 case q.lastRuneInSingleLineCommentMarker: 138 // TODO: Simplify by checking q.None() first, and assuming that the len of the marker is > 1 if it's not 1 since it's not 0 139 if !q.multiLineComment && !q.singleLineComment && !q.startedMultiLineString && prevPrevRune != ':' && q.doubleQuote == 0 && q.singleQuote == 0 && q.backtick == 0 { 140 switch { 141 case len(q.singleLineCommentMarkerRunes) == 1: 142 fallthrough 143 case len(q.singleLineCommentMarkerRunes) > 1 && prevRune == q.firstRuneInSingleLineCommentMarker: 144 q.singleLineComment = true 145 q.startedMultiLineString = false 146 q.stoppedMultiLineComment = false 147 q.multiLineComment = false 148 q.backtick = 0 149 q.doubleQuote = 0 150 q.singleQuote = 0 151 // We're in a single line comment, nothing more to do for this line 152 return 153 } 154 } 155 if r != '/' { 156 break 157 } 158 // r == '/' 159 fallthrough 160 case '/': // support C-style multi-line comments 161 if q.firstRuneInSingleLineCommentMarker != '#' && prevRune == '*' { 162 q.stoppedMultiLineComment = true 163 q.multiLineComment = false 164 if q.startedMultiLineComment { 165 q.containsMultiLineComments = true 166 } 167 } 168 case '(': 169 if q.None() { 170 q.parCount++ 171 } 172 case ')': 173 if (q.mode == mode.StandardML || q.mode == mode.OCaml) && prevRune == '*' { 174 q.stoppedMultiLineComment = true 175 q.multiLineComment = false 176 if q.startedMultiLineComment { 177 q.containsMultiLineComments = true 178 } 179 } else if q.None() { 180 q.parCount-- 181 } 182 case '[': 183 if q.None() { 184 q.braCount++ 185 } 186 case ']': 187 if q.None() { 188 q.braCount-- 189 } 190 case '>': // support HTML-style and XML-style multi-line comments 191 if prevRune == '-' && (q.mode == mode.HTML || q.mode == mode.XML) { 192 q.stoppedMultiLineComment = true 193 q.multiLineComment = false 194 if q.startedMultiLineComment { 195 q.containsMultiLineComments = true 196 } 197 } 198 } 199} 200 201// Process takes a line of text and modifies the current quote state accordingly, 202// depending on which runes are encountered. 203func (q *QuoteState) Process(line string) (rune, rune) { 204 q.singleLineComment = false 205 q.startedMultiLineString = false 206 q.stoppedMultiLineComment = false 207 q.containsMultiLineComments = false 208 prevRune := '\n' 209 prevPrevRune := '\n' 210 for _, r := range line { 211 q.ProcessRune(r, prevRune, prevPrevRune) 212 prevPrevRune = prevRune 213 prevRune = r 214 } 215 return prevRune, prevPrevRune 216} 217 218// ParBraCount will count the parenthesis and square brackets for a single line 219// while skipping comments and multiline strings 220// and without modifying the QuoteState. 221func (q *QuoteState) ParBraCount(line string) (int, int) { 222 qCopy := *q 223 qCopy.parCount = 0 224 qCopy.braCount = 0 225 qCopy.Process(line) 226 return qCopy.parCount, qCopy.braCount 227} 228