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