Welcome to mirror list, hosted at ThFree Co, Russian Federation.

SieveParser.mjs « toolkit « libSieve « common « src - github.com/thsmi/sieve.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: f68b51f293b58b8a431ec9a0c989d380aa528310 (plain)
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
/*
 * The contents of this file are licensed. You may obtain a copy of
 * the license at https://github.com/thsmi/sieve/ or request it via
 * email from the author.
 *
 * Do not remove or change this comment.
 *
 * The initial author of the code is:
 *   Thomas Schmid <schmid-thomas@gmx.net>
 *
 */

/** the number of bytes to quote in case of an error*/
const QUOTE_LENGTH = 50;
const NO_SUCH_TOKEN = -1;

const IDX_BEGIN = 0;
const ONE_CHARACTER = 1;
/**
 * A linear string parser used to tokenize and extract a string into atoms.
 */
class SieveParser {

  /**
   * Initializes the instance.
   *
   * @param {string} data
   *   the data which shall be parsed
   */
  constructor(data) {
    this._data = data;
    this._pos = IDX_BEGIN;
  }

  /**
   * Checks it the character at the given position matches the given whitelist.
   *
   * @param {char | char[]} ch
   *   a character or an array or characters which should matches the current position.
   * @param {int} [offset]
   *   an optional offset. Relative to the current position.
   *   if omitted no offset is used.
   * @returns {boolean}
   *   true in case the current character matches otherwise false
   */
  isChar(ch, offset) {
    if (typeof (offset) === "undefined" || offset === null)
      offset = IDX_BEGIN;

    if (!Array.isArray(ch))
      return (this._data.charAt(this._pos + offset) === ch);

    ch = [].concat(ch);

    for (const item of ch)
      if (this._data.charAt(this._pos + offset) === item)
        return true;

    return false;
  }

  /**
   * Extracts a token terminated by and of the given delimiters.
   * In case the delimiter is not found an exception is thrown.
   *
   * @param {char |char []} delimiter
   *   the delimiters which terminate the token.
   * @returns {string}
   *   the token as string.
   */
  extractToken(delimiter) {
    let offset = IDX_BEGIN;

    while (this.isChar(delimiter, offset))
      offset++;

    if (offset === IDX_BEGIN)
      throw new Error("Delimiter >>" + delimiter + "<< expected but found\n" + this.bytes(QUOTE_LENGTH) + "...");

    const str = this._data.substr(this._pos, offset);

    this._pos += str.length;

    return str;
  }

  /**
   * Extracts a single character
   *
   * @param {char|char[]} [ch]
   *   the extracted character has to be one of the given characters
   *   otherwise an exception is thrown.
   *   If omits any character will match.
   *
   * @returns {char}
   *   the extracted character.
   */
  extractChar(ch) {
    if (typeof (ch) !== "undefined" && ch !== null)
      if (!this.isChar(ch))
        throw new Error("" + ch + " expected but found:\n" + this.bytes(QUOTE_LENGTH) + "...");

    this._pos++;
    return this._data.charAt(this._pos - ONE_CHARACTER);
  }

  /**
   * Skips a single character.
   *
   * @param {char|char[]} [ch]
   *   The chars which are expected.
   *   If omitted any char will match.
   *
   * @returns {boolean}
   *   true in case the char was skipped otherwise false.
   */
  skipChar(ch) {
    if (typeof (ch) !== "undefined" || ch !== null)
      if (!this.isChar(ch))
        return false;

    this._pos++;
    return true;
  }

  /**
   *  Checks if the current puffer starts with the given token(s)
   *
   *  @param {string|string[]} tokens
   *    the tokens which should be checked
   *
   *  @param {boolean} [ignoreCase]
   *    optional boolean argument default to true
   *
   *  @returns {boolean}
   *    true in case the string starts with one of the tokens
   *    otherwise false
   */
  startsWith(tokens, ignoreCase) {

    if (typeof (ignoreCase) === "undefined" || ignoreCase === null)
      ignoreCase = true;

    if (!Array.isArray(tokens))
      tokens = [tokens];

    for (let token of tokens) {
      let data = this._data.substr(this._pos, token.length);

      if (ignoreCase) {
        token = token.toLowerCase();
        data = data.toLowerCase();
      }

      if (data === token)
        return true;
    }

    return false;
  }

  /**
   * Extracts and/or skips the given number of bytes.
   *
   * You can either pass an integer with the absolute number of bytes or a string.
   *
   * In case you pass a string, the string length will be skipped. But only in case
   * it matches case insensitive. Otherwise an exception is thrown.
   *
   * In case the length parameter is neither a string nor a parameter and exception is thrown.
   *
   * @param {int|string} length
   *   Can be an integer which defines an absolute number of bytes which should be skipped.
   *   Or a string, which will be matched case sensitive and extracted.
   *
   * @returns {string}
   *   the extracted string
   */
  extract(length) {
    let result = null;

    if (typeof (length) === "string") {
      const str = length;

      if (!this.startsWith(str))
        throw new Error("" + str + " expected but found:\n" + this.bytes(QUOTE_LENGTH) + "...");

      result = this.bytes(str.length);
      this._pos += str.length;

      return result;
    }

    if (Number.isNaN(Number.parseInt(length, 10)))
      throw new Error("Extract failed, length parameter is not a number");

    result = this.bytes(length);
    this._pos += length;

    return result;
  }

  /**
   * Searches for the given token.
   * If found all data before the token is returned and
   * the buffer is advanced to the end of the token.
   *
   * In case the token is not found in the buffer an
   * exception will be thrown
   *
   * @param {string} token
   *   the token which is used as delimiter
   * @returns {string}
   *   the data between the token an the delimiter
   */
  extractUntil(token) {
    const idx = this._data.indexOf(token, this._pos);

    if (idx === NO_SUCH_TOKEN)
      throw new Error("Token expected: " + token.toString());

    const str = this._data.substring(this._pos, idx);

    this._pos += str.length + token.length;

    return str;
  }

  /**
   * Checks if the current character is numeric.
   *
   * @param {int} [offset]
   *   defaults to zero if omitted
   *
   * @returns {boolean}
   *   true in case the current character is numeric otherwise false.
   */
  isNumber(offset) {
    if (typeof (offset) !== "number")
      offset = IDX_BEGIN;

    if (this._pos + offset > this._data.length)
      throw new Error("Parser out of bounds");

    return !Number.isNaN(Number.parseInt(this._data.charAt(this._pos + offset), 10));
  }

  /**
   * Extracts an integer value starting from the current position.
   *
   * @returns {int}
   *  the extracted number.
   */
  extractNumber() {
    let offset = IDX_BEGIN;
    while (this.isNumber(offset))
      offset++;

    if (offset === IDX_BEGIN)
      throw new Error("Number expected but found:\n" + this.bytes(QUOTE_LENGTH) + "...");

    const number = this._data.substr(this._pos, offset);

    this._pos += offset;

    return number;
  }

  /**
   * Returns the remaining buffer.
   * @param {int} [length]
   *   optional returns at lost length bytes.
   *   if omitted the complete buffer is returned
   * @returns {string}
   *   the buffer data.
   */
  bytes(length) {
    return this._data.substr(this._pos, length);
  }

  /**
   * Checks if the internal string buffer is depleted.
   * Which means it reached the end of the string.
   *
   * @returns {boolean}
   *   true in case the buffer is empty. Otherwise false.
   */
  empty() {
    return (this._pos >= this._data.length);
  }

  /**
   * Rewinds the internal buffer by the given number of bytes
   * @param {int} offset
   *   the number of bytes to rewind.
   *
   */
  rewind(offset) {
    this._pos -= offset;

    if (this._pos < IDX_BEGIN)
      this._pos = IDX_BEGIN;
  }

  /**
   * Gets or sets te current position.
   *
   * @param {int} position
   *   the new position which should be set. You can't exceed the buffer length.
   * @returns {int}
   *   the current position.
   */
  pos(position) {
    if (typeof (position) === "number")
      this._pos = position;

    if (this._pos > this._data.length)
      this._pos = this._data.length;

    if (this._pos < IDX_BEGIN)
      this._pos = IDX_BEGIN;

    return this._pos;
  }
}

export { SieveParser };