-
-
Notifications
You must be signed in to change notification settings - Fork 352
/
TokenQueue.swift
429 lines (385 loc) · 12.8 KB
/
TokenQueue.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
//
// TokenQueue.swift
// SwiftSoup
//
// Created by Nabil Chatbi on 13/10/16.
// Copyright © 2016 Nabil Chatbi.. All rights reserved.
//
import Foundation
open class TokenQueue {
private var queue: String
private var pos: Int = 0
private static let empty: Character = Character(UnicodeScalar(0))
private static let ESC: Character = "\\" // escape char for chomp balanced.
/**
Create a new TokenQueue.
@param data string of data to back queue.
*/
public init (_ data: String) {
queue = data
}
/**
* Is the queue empty?
* @return true if no data left in queue.
*/
open func isEmpty() -> Bool {
return remainingLength() == 0
}
private func remainingLength() -> Int {
return queue.count - pos
}
/**
* Retrieves but does not remove the first character from the queue.
* @return First character, or 0 if empty.
*/
open func peek() -> Character {
return isEmpty() ? Character(UnicodeScalar(0)) : queue[pos]
}
/**
Add a character to the start of the queue (will be the next character retrieved).
@param c character to add
*/
open func addFirst(_ c: Character) {
addFirst(String(c))
}
/**
Add a string to the start of the queue.
@param seq string to add.
*/
open func addFirst(_ seq: String) {
// not very performant, but an edge case
queue = seq + queue.substring(pos)
pos = 0
}
/**
* Tests if the next characters on the queue match the sequence. Case insensitive.
* @param seq String to check queue for.
* @return true if the next characters match.
*/
open func matches(_ seq: String) -> Bool {
return queue.regionMatches(ignoreCase: true, selfOffset: pos,
other: seq, otherOffset: 0, targetLength: seq.count)
}
/**
* Case sensitive match test.
* @param seq string to case sensitively check for
* @return true if matched, false if not
*/
open func matchesCS(_ seq: String) -> Bool {
return queue.startsWith(seq, pos)
}
/**
Tests if the next characters match any of the sequences. Case insensitive.
@param seq list of strings to case insensitively check for
@return true of any matched, false if none did
*/
open func matchesAny(_ seq: [String]) -> Bool {
for s in seq {
if (matches(s)) {
return true
}
}
return false
}
open func matchesAny(_ seq: String...) -> Bool {
return matchesAny(seq)
}
open func matchesAny(_ seq: Character...) -> Bool {
if (isEmpty()) {
return false
}
for c in seq {
if (queue[pos] as Character == c) {
return true
}
}
return false
}
open func matchesStartTag() -> Bool {
// micro opt for matching "<x"
return (remainingLength() >= 2 && queue[pos] as Character == "<" && Character.isLetter(queue.charAt(pos+1)))
}
/**
* Tests if the queue matches the sequence (as with match), and if they do, removes the matched string from the
* queue.
* @param seq String to search for, and if found, remove from queue.
* @return true if found and removed, false if not found.
*/
@discardableResult
open func matchChomp(_ seq: String) -> Bool {
if (matches(seq)) {
pos += seq.count
return true
} else {
return false
}
}
/**
Tests if queue starts with a whitespace character.
@return if starts with whitespace
*/
open func matchesWhitespace() -> Bool {
return !isEmpty() && StringUtil.isWhitespace(queue.charAt(pos))
}
/**
Test if the queue matches a word character (letter or digit).
@return if matches a word character
*/
open func matchesWord() -> Bool {
return !isEmpty() && (Character.isLetterOrDigit(queue.charAt(pos)))
}
/**
* Drops the next character off the queue.
*/
open func advance() {
if (!isEmpty()) {pos+=1}
}
/**
* Consume one character off queue.
* @return first character on queue.
*/
open func consume() -> Character {
let i = pos
pos+=1
return queue.charAt(i)
}
/**
* Consumes the supplied sequence of the queue. If the queue does not start with the supplied sequence, will
* throw an illegal state exception -- but you should be running match() against that condition.
<p>
Case insensitive.
* @param seq sequence to remove from head of queue.
*/
open func consume(_ seq: String)throws {
if (!matches(seq)) {
//throw new IllegalStateException("Queue did not match expected sequence")
throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: "Queue did not match expected sequence")
}
let len = seq.count
if (len > remainingLength()) {
//throw new IllegalStateException("Queue not long enough to consume sequence")
throw Exception.Error(type: ExceptionType.IllegalArgumentException, Message: "Queue not long enough to consume sequence")
}
pos += len
}
/**
* Pulls a string off the queue, up to but exclusive of the match sequence, or to the queue running out.
* @param seq String to end on (and not include in return, but leave on queue). <b>Case sensitive.</b>
* @return The matched data consumed from queue.
*/
@discardableResult
open func consumeTo(_ seq: String) -> String {
let offset = queue.indexOf(seq, pos)
if (offset != -1) {
let consumed = queue.substring(pos, offset-pos)
pos += consumed.count
return consumed
} else {
//return remainder()
}
return ""
}
open func consumeToIgnoreCase(_ seq: String) -> String {
let start = pos
let first = seq.substring(0, 1)
let canScan = first.lowercased() == first.uppercased() // if first is not cased, use index of
while (!isEmpty()) {
if (matches(seq)) {
break
}
if (canScan) {
let skip = queue.indexOf(first, pos) - pos
if (skip == 0) { // this char is the skip char, but not match, so force advance of pos
pos+=1
} else if (skip < 0) { // no chance of finding, grab to end
pos = queue.count
} else {
pos += skip
}
} else {
pos+=1
}
}
return queue.substring(start, pos-start)
}
/**
Consumes to the first sequence provided, or to the end of the queue. Leaves the terminator on the queue.
@param seq any number of terminators to consume to. <b>Case insensitive.</b>
@return consumed string
*/
// todo: method name. not good that consumeTo cares for case, and consume to any doesn't. And the only use for this
// is is a case sensitive time...
open func consumeToAny(_ seq: String...) -> String {
return consumeToAny(seq)
}
open func consumeToAny(_ seq: [String]) -> String {
let start = pos
while (!isEmpty() && !matchesAny(seq)) {
pos+=1
}
return queue.substring(start, pos-start)
}
/**
* Pulls a string off the queue (like consumeTo), and then pulls off the matched string (but does not return it).
* <p>
* If the queue runs out of characters before finding the seq, will return as much as it can (and queue will go
* isEmpty() == true).
* @param seq String to match up to, and not include in return, and to pull off queue. <b>Case sensitive.</b>
* @return Data matched from queue.
*/
open func chompTo(_ seq: String) -> String {
let data = consumeTo(seq)
matchChomp(seq)
return data
}
open func chompToIgnoreCase(_ seq: String) -> String {
let data = consumeToIgnoreCase(seq) // case insensitive scan
matchChomp(seq)
return data
}
/**
* Pulls a balanced string off the queue. E.g. if queue is "(one (two) three) four", (,) will return "one (two) three",
* and leave " four" on the queue. Unbalanced openers and closers can quoted (with ' or ") or escaped (with \). Those escapes will be left
* in the returned string, which is suitable for regexes (where we need to preserve the escape), but unsuitable for
* contains text strings; use unescape for that.
* @param open opener
* @param close closer
* @return data matched from the queue
*/
open func chompBalanced(_ open: Character, _ close: Character) -> String {
var start = -1
var end = -1
var depth = 0
var last: Character = TokenQueue.empty
var inQuote = false
repeat {
if (isEmpty()) {break}
let c = consume()
if (last == TokenQueue.empty || last != TokenQueue.ESC) {
if ((c=="'" || c=="\"") && c != open) {
inQuote = !inQuote
}
if (inQuote) {
continue
}
if (c==open) {
depth+=1
if (start == -1) {
start = pos
}
} else if (c==close) {
depth-=1
}
}
if (depth > 0 && last != TokenQueue.empty) {
end = pos // don't include the outer match pair in the return
}
last = c
} while (depth > 0)
return (end >= 0) ? queue.substring(start, end-start) : ""
}
/**
* Unescaped a \ escaped string.
* @param in backslash escaped string
* @return unescaped string
*/
public static func unescape(_ input: String) -> String {
let out = StringBuilder()
var last = empty
for c in input {
if (c == ESC) {
if (last != empty && last == TokenQueue.ESC) {
out.append(c)
}
} else {
out.append(c)
}
last = c
}
return out.toString()
}
/**
* Pulls the next run of whitespace characters of the queue.
* @return Whether consuming whitespace or not
*/
@discardableResult
open func consumeWhitespace() -> Bool {
var seen = false
while (matchesWhitespace()) {
pos+=1
seen = true
}
return seen
}
/**
* Retrieves the next run of word type (letter or digit) off the queue.
* @return String of word characters from queue, or empty string if none.
*/
@discardableResult
open func consumeWord() -> String {
let start = pos
while (matchesWord()) {
pos+=1
}
return queue.substring(start, pos-start)
}
/**
* Consume an tag name off the queue (word or :, _, -)
*
* @return tag name
*/
open func consumeTagName() -> String {
let start = pos
while (!isEmpty() && (matchesWord() || matchesAny(":", "_", "-"))) {
pos+=1
}
return queue.substring(start, pos-start)
}
/**
* Consume a CSS element selector (tag name, but | instead of : for namespaces (or *| for wildcard namespace), to not conflict with :pseudo selects).
*
* @return tag name
*/
open func consumeElementSelector() -> String {
let start = pos
while (!isEmpty() && (matchesWord() || matchesAny("*|", "|", "_", "-"))) {
pos+=1
}
return queue.substring(start, pos-start)
}
/**
Consume a CSS identifier (ID or class) off the queue (letter, digit, -, _)
http://www.w3.org/TR/CSS2/syndata.html#value-def-identifier
@return identifier
*/
open func consumeCssIdentifier() -> String {
let start = pos
while (!isEmpty() && (matchesWord() || matchesAny("-", "_"))) {
pos+=1
}
return queue.substring(start, pos-start)
}
/**
Consume an attribute key off the queue (letter, digit, -, _, :")
@return attribute key
*/
open func consumeAttributeKey() -> String {
let start = pos
while (!isEmpty() && (matchesWord() || matchesAny("-", "_", ":"))) {
pos+=1
}
return queue.substring(start, pos-start)
}
/**
Consume and return whatever is left on the queue.
@return remained of queue.
*/
open func remainder() -> String {
let remainder = queue.substring(pos, queue.count-pos)
pos = queue.count
return remainder
}
open func toString() -> String {
return queue.substring(pos)
}
}