postal-mime 2.4.6 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.prettierignore CHANGED
@@ -6,4 +6,5 @@ coverage
6
6
  .DS_Store
7
7
  yarn.lock
8
8
  package-lock.json
9
- CLAUDE.md
9
+ CLAUDE.md
10
+ CHANGELOG.md
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.5.0](https://github.com/postalsys/postal-mime/compare/v2.4.7...v2.5.0) (2025-10-07)
4
+
5
+
6
+ ### Features
7
+
8
+ * add comprehensive type validation and TypeScript support ([81a6467](https://github.com/postalsys/postal-mime/commit/81a6467b6c5379c502bac9ee7023e2c2dee976cb))
9
+
10
+ ## [2.4.7](https://github.com/postalsys/postal-mime/compare/v2.4.6...v2.4.7) (2025-10-07)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * prevent email extraction from quoted strings in addressParser ([837d679](https://github.com/postalsys/postal-mime/commit/837d679b48cde95a111b8508a7aea23a28cbc12a))
16
+
3
17
  ## [2.4.6](https://github.com/postalsys/postal-mime/compare/v2.4.5...v2.4.6) (2025-10-01)
4
18
 
5
19
 
package/README.md CHANGED
@@ -2,9 +2,18 @@
2
2
 
3
3
  **postal-mime** is an email parsing library that runs in browser environments (including Web Workers) and serverless functions (like Cloudflare Email Workers). It takes in a raw email message (RFC822 format) and outputs a structured object containing headers, recipients, attachments, and more.
4
4
 
5
- > **Tip**
5
+ > [!TIP]
6
6
  > PostalMime is developed by the makers of [EmailEngine](https://emailengine.app/?utm_source=github&utm_campaign=imapflow&utm_medium=readme-link)—a self-hosted email gateway that provides a REST API for IMAP and SMTP servers and sends webhooks whenever something changes in registered accounts.
7
7
 
8
+ ## Features
9
+
10
+ - **Browser & Node.js compatible** - Works in browsers, Web Workers, Node.js, and serverless environments
11
+ - **TypeScript support** - Fully typed with comprehensive type definitions
12
+ - **Zero dependencies** - No external dependencies
13
+ - **RFC compliant** - Follows RFC 2822/5322 email standards
14
+ - **Handles complex MIME structures** - Multipart messages, nested parts, attachments
15
+ - **Security limits** - Built-in protection against deeply nested messages and oversized headers
16
+
8
17
  ## Table of Contents
9
18
 
10
19
  - [Source](#source)
@@ -14,6 +23,7 @@
14
23
  - [Browser](#browser)
15
24
  - [Node.js](#nodejs)
16
25
  - [Cloudflare Email Workers](#cloudflare-email-workers)
26
+ - [TypeScript Support](#typescript-support)
17
27
  - [API](#api)
18
28
  - [PostalMime.parse()](#postalmimeparse)
19
29
  - [Utility Functions](#utility-functions)
@@ -58,6 +68,23 @@ Content-Type: text/html; charset=utf-8
58
68
  console.log(email.subject); // "My awesome email 🤓"
59
69
  ```
60
70
 
71
+ <details>
72
+ <summary><strong>TypeScript</strong></summary>
73
+
74
+ ```typescript
75
+ import PostalMime from './node_modules/postal-mime/src/postal-mime.js';
76
+ import type { Email } from 'postal-mime';
77
+
78
+ const email: Email = await PostalMime.parse(`Subject: My awesome email 🤓
79
+ Content-Type: text/html; charset=utf-8
80
+
81
+ <p>Hello world 😵‍💫</p>`);
82
+
83
+ console.log(email.subject); // "My awesome email 🤓"
84
+ ```
85
+
86
+ </details>
87
+
61
88
  ### Node.js
62
89
 
63
90
  In Node.js (including serverless functions), import it directly from `postal-mime`:
@@ -75,6 +102,29 @@ Content-Type: text/html; charset=utf-8
75
102
  console.log(util.inspect(email, false, 22, true));
76
103
  ```
77
104
 
105
+ <details>
106
+ <summary><strong>TypeScript</strong></summary>
107
+
108
+ ```typescript
109
+ import PostalMime from 'postal-mime';
110
+ import type { Email, PostalMimeOptions } from 'postal-mime';
111
+ import util from 'node:util';
112
+
113
+ const options: PostalMimeOptions = {
114
+ attachmentEncoding: 'base64'
115
+ };
116
+
117
+ const email: Email = await PostalMime.parse(`Subject: My awesome email 🤓
118
+ Content-Type: text/html; charset=utf-8
119
+
120
+ <p>Hello world 😵‍💫</p>`, options);
121
+
122
+ // Use 'util.inspect' for pretty-printing
123
+ console.log(util.inspect(email, false, 22, true));
124
+ ```
125
+
126
+ </details>
127
+
78
128
  ### Cloudflare Email Workers
79
129
 
80
130
  Use the `message.raw` as the raw email data for parsing:
@@ -93,6 +143,77 @@ export default {
93
143
  };
94
144
  ```
95
145
 
146
+ <details>
147
+ <summary><strong>TypeScript</strong></summary>
148
+
149
+ ```typescript
150
+ import PostalMime from 'postal-mime';
151
+ import type { Email } from 'postal-mime';
152
+
153
+ export default {
154
+ async email(message: ForwardableEmailMessage, env: Env, ctx: ExecutionContext): Promise<void> {
155
+ const email: Email = await PostalMime.parse(message.raw);
156
+
157
+ console.log('Subject:', email.subject);
158
+ console.log('HTML:', email.html);
159
+ console.log('Text:', email.text);
160
+ }
161
+ };
162
+ ```
163
+
164
+ </details>
165
+
166
+ ---
167
+
168
+ ## TypeScript Support
169
+
170
+ PostalMime includes comprehensive TypeScript type definitions. All types are exported and can be imported from the main package:
171
+
172
+ ```typescript
173
+ import PostalMime, { addressParser, decodeWords } from 'postal-mime';
174
+ import type {
175
+ Email,
176
+ Address,
177
+ Mailbox,
178
+ Header,
179
+ Attachment,
180
+ PostalMimeOptions,
181
+ AddressParserOptions,
182
+ RawEmail
183
+ } from 'postal-mime';
184
+ ```
185
+
186
+ > [!NOTE]
187
+ > PostalMime is written in JavaScript but provides comprehensive TypeScript type definitions. All types are validated through both compile-time type checking and runtime type validation tests to ensure accuracy.
188
+
189
+ ### Available Types
190
+
191
+ - **`Email`** - The main parsed email object returned by `PostalMime.parse()`
192
+ - **`Address`** - Union type representing either a `Mailbox` or an address group
193
+ - **`Mailbox`** - Individual email address with name and address fields
194
+ - **`Header`** - Email header with key and value
195
+ - **`Attachment`** - Email attachment with metadata and content
196
+ - **`PostalMimeOptions`** - Configuration options for parsing
197
+ - **`AddressParserOptions`** - Configuration options for address parsing
198
+ - **`RawEmail`** - Union type for all accepted email input formats
199
+
200
+ ### Type Narrowing
201
+
202
+ TypeScript users can use type guards to narrow address types:
203
+
204
+ ```typescript
205
+ import type { Address, Mailbox } from 'postal-mime';
206
+
207
+ function isMailbox(addr: Address): addr is Mailbox {
208
+ return !('group' in addr) || addr.group === undefined;
209
+ }
210
+
211
+ // Usage
212
+ if (email.from && isMailbox(email.from)) {
213
+ console.log(email.from.address); // TypeScript knows this is a Mailbox
214
+ }
215
+ ```
216
+
96
217
  ---
97
218
 
98
219
  ## API
@@ -100,7 +221,7 @@ export default {
100
221
  ### PostalMime.parse()
101
222
 
102
223
  ```js
103
- PostalMime.parse(email, options) -> Promise<ParsedEmail>
224
+ PostalMime.parse(email, options) -> Promise<Email>
104
225
  ```
105
226
 
106
227
  - **email**: An RFC822 formatted email. This can be a `string`, `ArrayBuffer/Uint8Array`, `Blob` (browser only), `Buffer` (Node.js), or a [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream).
@@ -114,29 +235,86 @@ PostalMime.parse(email, options) -> Promise<ParsedEmail>
114
235
  - **maxNestingDepth** (number, default: `256`): Maximum allowed MIME part nesting depth. Throws an error if exceeded.
115
236
  - **maxHeadersSize** (number, default: `2097152`): Maximum allowed total header size in bytes (default 2MB). Throws an error if exceeded.
116
237
 
117
- **Returns**: A Promise that resolves to a structured object with the following properties:
238
+ > [!IMPORTANT]
239
+ > The `maxNestingDepth` and `maxHeadersSize` options provide built-in security against malicious emails with deeply nested MIME structures or oversized headers that could cause performance issues or memory exhaustion.
240
+
241
+ **Returns**: A Promise that resolves to a structured `Email` object with the following properties:
118
242
 
119
- - **headers**: An array of header objects, each containing:
243
+ - **headers**: An array of `Header` objects, each containing:
120
244
  - `key`: Lowercase header name (e.g., `"dkim-signature"`).
121
245
  - `value`: Unprocessed header value as a string.
122
- - **from**, **sender**: Processed address objects:
246
+ - **from**, **sender**: Processed `Address` objects (can be a `Mailbox` or address group):
123
247
  - `name`: Decoded display name, or an empty string if not set.
124
248
  - `address`: Email address.
249
+ - `group`: Array of `Mailbox` objects (only for address groups).
125
250
  - **deliveredTo**, **returnPath**: Single email addresses as strings.
126
- - **to**, **cc**, **bcc**, **replyTo**: Arrays of processed address objects (same structure as `from`).
251
+ - **to**, **cc**, **bcc**, **replyTo**: Arrays of `Address` objects (same structure as `from`).
127
252
  - **subject**: Subject line of the email.
128
253
  - **messageId**, **inReplyTo**, **references**: Values from their corresponding headers.
129
- - **date**: The emails sending time in ISO 8601 format (or the original string if parsing fails).
254
+ - **date**: The email's sending time in ISO 8601 format (or the original string if parsing fails).
130
255
  - **html**: String containing the HTML content of the email.
131
256
  - **text**: String containing the plain text content of the email.
132
- - **attachments**: Array of attachment objects:
133
- - `filename`
134
- - `mimeType`
135
- - `disposition` (e.g., `"attachment"`, `"inline"`, or `null`)
136
- - `related` (boolean, `true` if its an inline image)
137
- - `contentId`
138
- - `content` (array buffer or string, depending on `attachmentEncoding`)
139
- - `encoding` (e.g., `"base64"`)
257
+ - **attachments**: Array of `Attachment` objects:
258
+ - `filename`: String or `null`
259
+ - `mimeType`: String
260
+ - `disposition`: `"attachment"`, `"inline"`, or `null`
261
+ - `related`: Boolean (optional, `true` if it's an inline image)
262
+ - `contentId`: String (optional)
263
+ - `content`: `ArrayBuffer` or string, depending on `attachmentEncoding`
264
+ - `encoding`: `"base64"` or `"utf8"` (optional)
265
+
266
+ <details>
267
+ <summary><strong>TypeScript Types</strong></summary>
268
+
269
+ ```typescript
270
+ import type {
271
+ Email,
272
+ Address,
273
+ Mailbox,
274
+ Header,
275
+ Attachment,
276
+ PostalMimeOptions,
277
+ RawEmail
278
+ } from 'postal-mime';
279
+
280
+ // Main email parsing
281
+ const email: Email = await PostalMime.parse(rawEmail);
282
+
283
+ // With options
284
+ const options: PostalMimeOptions = {
285
+ attachmentEncoding: 'base64',
286
+ maxNestingDepth: 100
287
+ };
288
+ const email: Email = await PostalMime.parse(rawEmail, options);
289
+
290
+ // Working with addresses
291
+ if (email.from) {
292
+ // Address can be either a Mailbox or a Group
293
+ if ('group' in email.from && email.from.group) {
294
+ // It's a group
295
+ email.from.group.forEach((member: Mailbox) => {
296
+ console.log(member.address);
297
+ });
298
+ } else {
299
+ // It's a mailbox
300
+ const mailbox = email.from as Mailbox;
301
+ console.log(mailbox.address);
302
+ }
303
+ }
304
+
305
+ // Working with attachments
306
+ email.attachments.forEach((att: Attachment) => {
307
+ if (att.encoding === 'base64') {
308
+ // content is a string
309
+ const base64Content: string = att.content as string;
310
+ } else {
311
+ // content is ArrayBuffer (default)
312
+ const buffer: ArrayBuffer = att.content as ArrayBuffer;
313
+ }
314
+ });
315
+ ```
316
+
317
+ </details>
140
318
 
141
319
  ---
142
320
 
@@ -147,23 +325,42 @@ PostalMime.parse(email, options) -> Promise<ParsedEmail>
147
325
  ```js
148
326
  import { addressParser } from 'postal-mime';
149
327
 
150
- addressParser(addressStr, opts) -> Array
328
+ addressParser(addressStr, opts) -> Address[]
151
329
  ```
152
330
 
153
331
  - **addressStr**: A raw address header string.
154
332
  - **opts**: Optional configuration:
155
333
  - **flatten** (boolean, default: `false`): If `true`, ignores address groups and returns a flat array of addresses.
156
334
 
157
- **Returns**: An array of address objects, which can be nested if address groups are present.
335
+ **Returns**: An array of `Address` objects, which can be nested if address groups are present.
158
336
 
159
337
  **Example**:
160
338
 
161
339
  ```js
340
+ import { addressParser } from 'postal-mime';
341
+
162
342
  const addressStr = '=?utf-8?B?44Ko44Od44K544Kr44O844OJ?= <support@example.com>';
163
343
  console.log(addressParser(addressStr));
164
344
  // [ { name: 'エポスカード', address: 'support@example.com' } ]
165
345
  ```
166
346
 
347
+ <details>
348
+ <summary><strong>TypeScript</strong></summary>
349
+
350
+ ```typescript
351
+ import { addressParser } from 'postal-mime';
352
+ import type { Address, AddressParserOptions } from 'postal-mime';
353
+
354
+ const addressStr = '=?utf-8?B?44Ko44Od44K544Kr44O844OJ?= <support@example.com>';
355
+ const addresses: Address[] = addressParser(addressStr);
356
+
357
+ // With options
358
+ const options: AddressParserOptions = { flatten: true };
359
+ const flatAddresses: Address[] = addressParser(addressStr, options);
360
+ ```
361
+
362
+ </details>
363
+
167
364
  #### decodeWords()
168
365
 
169
366
  ```js
@@ -179,11 +376,26 @@ decodeWords(encodedStr) -> string
179
376
  **Example**:
180
377
 
181
378
  ```js
379
+ import { decodeWords } from 'postal-mime';
380
+
182
381
  const encodedStr = 'Hello, =?utf-8?B?44Ko44Od44K544Kr44O844OJ?=';
183
382
  console.log(decodeWords(encodedStr));
184
383
  // Hello, エポスカード
185
384
  ```
186
385
 
386
+ <details>
387
+ <summary><strong>TypeScript</strong></summary>
388
+
389
+ ```typescript
390
+ import { decodeWords } from 'postal-mime';
391
+
392
+ const encodedStr = 'Hello, =?utf-8?B?44Ko44Od44K544Kr44O844OJ?=';
393
+ const decoded: string = decodeWords(encodedStr);
394
+ console.log(decoded); // Hello, エポスカード
395
+ ```
396
+
397
+ </details>
398
+
187
399
  ---
188
400
 
189
401
  ## License
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postal-mime",
3
- "version": "2.4.6",
3
+ "version": "2.5.0",
4
4
  "description": "Email parser for browser environments",
5
5
  "main": "./src/postal-mime.js",
6
6
  "exports": {
@@ -11,7 +11,9 @@
11
11
  "type": "module",
12
12
  "types": "postal-mime.d.ts",
13
13
  "scripts": {
14
- "test": "eslint && node --test",
14
+ "test": "npm run lint && npm run type-check && node --test",
15
+ "lint": "eslint",
16
+ "type-check": "tsc --noEmit",
15
17
  "update": "rm -rf node_modules package-lock.json && ncu -u && npm install",
16
18
  "format": "prettier --write \"**/*.{js,mjs,json}\" --ignore-path .prettierignore",
17
19
  "format:check": "prettier --check \"**/*.{js,mjs,json}\" --ignore-path .prettierignore"
@@ -36,6 +38,7 @@
36
38
  "eslint": "8.57.0",
37
39
  "eslint-cli": "1.1.1",
38
40
  "iframe-resizer": "4.3.6",
39
- "prettier": "^3.6.2"
41
+ "prettier": "^3.6.2",
42
+ "typescript": "^5.9.3"
40
43
  }
41
44
  }
package/postal-mime.d.ts CHANGED
@@ -5,12 +5,20 @@ export type Header = {
5
5
  value: string;
6
6
  };
7
7
 
8
- export type Address = {
8
+ export type Mailbox = {
9
9
  name: string;
10
- address?: string;
11
- group?: Address[]
10
+ address: string;
11
+ group?: undefined;
12
12
  };
13
13
 
14
+ export type Address =
15
+ | Mailbox
16
+ | {
17
+ name: string;
18
+ address?: undefined;
19
+ group: Mailbox[];
20
+ };
21
+
14
22
  export type Attachment = {
15
23
  filename: string | null;
16
24
  mimeType: string;
@@ -43,20 +51,20 @@ export type Email = {
43
51
  attachments: Attachment[];
44
52
  };
45
53
 
46
- declare type AddressParserOptions = {
54
+ export type AddressParserOptions = {
47
55
  flatten?: boolean
48
56
  }
49
57
 
50
- declare function addressParser (
58
+ export function addressParser (
51
59
  str: string,
52
60
  options?: AddressParserOptions
53
61
  ): Address[];
54
62
 
55
- declare function decodeWords (
63
+ export function decodeWords (
56
64
  str: string
57
65
  ): string;
58
66
 
59
- declare type PostalMimeOptions = {
67
+ export type PostalMimeOptions = {
60
68
  rfc822Attachments?: boolean,
61
69
  forceRfc822Attachments?: boolean,
62
70
  attachmentEncoding?: "base64" | "utf8" | "arraybuffer",
@@ -64,7 +72,7 @@ declare type PostalMimeOptions = {
64
72
  maxHeadersSize?: number
65
73
  }
66
74
 
67
- declare class PostalMime {
75
+ export default class PostalMime {
68
76
  constructor(options?: PostalMimeOptions);
69
77
  static parse(
70
78
  email: RawEmail,
@@ -72,6 +80,3 @@ declare class PostalMime {
72
80
  ): Promise<Email>;
73
81
  parse(email: RawEmail): Promise<Email>;
74
82
  }
75
-
76
- export { addressParser, decodeWords };
77
- export default PostalMime;
@@ -7,7 +7,6 @@ import { decodeWords } from './decode-strings.js';
7
7
  * @return {Object} Address object
8
8
  */
9
9
  function _handleAddress(tokens) {
10
- let token;
11
10
  let isGroup = false;
12
11
  let state = 'text';
13
12
  let address;
@@ -16,28 +15,41 @@ function _handleAddress(tokens) {
16
15
  address: [],
17
16
  comment: [],
18
17
  group: [],
19
- text: []
18
+ text: [],
19
+ textWasQuoted: [] // Track which text tokens came from inside quotes
20
20
  };
21
21
  let i;
22
22
  let len;
23
+ let insideQuotes = false; // Track if we're currently inside a quoted string
23
24
 
24
25
  // Filter out <addresses>, (comments) and regular text
25
26
  for (i = 0, len = tokens.length; i < len; i++) {
26
- token = tokens[i];
27
+ let token = tokens[i];
28
+ let prevToken = i ? tokens[i - 1] : null;
27
29
  if (token.type === 'operator') {
28
30
  switch (token.value) {
29
31
  case '<':
30
32
  state = 'address';
33
+ insideQuotes = false;
31
34
  break;
32
35
  case '(':
33
36
  state = 'comment';
37
+ insideQuotes = false;
34
38
  break;
35
39
  case ':':
36
40
  state = 'group';
37
41
  isGroup = true;
42
+ insideQuotes = false;
43
+ break;
44
+ case '"':
45
+ // Track quote state for text tokens
46
+ insideQuotes = !insideQuotes;
47
+ state = 'text';
38
48
  break;
39
49
  default:
40
50
  state = 'text';
51
+ insideQuotes = false;
52
+ break;
41
53
  }
42
54
  } else if (token.value) {
43
55
  if (state === 'address') {
@@ -46,7 +58,19 @@ function _handleAddress(tokens) {
46
58
  // and so will we
47
59
  token.value = token.value.replace(/^[^<]*<\s*/, '');
48
60
  }
49
- data[state].push(token.value);
61
+
62
+ if (prevToken && prevToken.noBreak && data[state].length) {
63
+ // join values
64
+ data[state][data[state].length - 1] += token.value;
65
+ if (state === 'text' && insideQuotes) {
66
+ data.textWasQuoted[data.textWasQuoted.length - 1] = true;
67
+ }
68
+ } else {
69
+ data[state].push(token.value);
70
+ if (state === 'text') {
71
+ data.textWasQuoted.push(insideQuotes);
72
+ }
73
+ }
50
74
  }
51
75
  }
52
76
 
@@ -59,16 +83,36 @@ function _handleAddress(tokens) {
59
83
  if (isGroup) {
60
84
  // http://tools.ietf.org/html/rfc2822#appendix-A.1.3
61
85
  data.text = data.text.join(' ');
86
+
87
+ // Parse group members, but flatten any nested groups (RFC 5322 doesn't allow nesting)
88
+ let groupMembers = [];
89
+ if (data.group.length) {
90
+ let parsedGroup = addressParser(data.group.join(','));
91
+ // Flatten: if any member is itself a group, extract its members into the sequence
92
+ parsedGroup.forEach(member => {
93
+ if (member.group) {
94
+ // Nested group detected - flatten it by adding its members directly
95
+ groupMembers = groupMembers.concat(member.group);
96
+ } else {
97
+ groupMembers.push(member);
98
+ }
99
+ });
100
+ }
101
+
62
102
  addresses.push({
63
103
  name: decodeWords(data.text || (address && address.name)),
64
- group: data.group.length ? addressParser(data.group.join(',')) : []
104
+ group: groupMembers
65
105
  });
66
106
  } else {
67
107
  // If no address was found, try to detect one from regular text
68
108
  if (!data.address.length && data.text.length) {
69
109
  for (i = data.text.length - 1; i >= 0; i--) {
70
- if (data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
110
+ // Security fix: Do not extract email addresses from quoted strings
111
+ // RFC 5321 allows @ inside quoted local-parts like "user@domain"@example.com
112
+ // Extracting emails from quoted text leads to misrouting vulnerabilities
113
+ if (!data.textWasQuoted[i] && data.text[i].match(/^[^@\s]+@[^@\s]+$/)) {
71
114
  data.address = data.text.splice(i, 1);
115
+ data.textWasQuoted.splice(i, 1);
72
116
  break;
73
117
  }
74
118
  }
@@ -85,10 +129,13 @@ function _handleAddress(tokens) {
85
129
  // still no address
86
130
  if (!data.address.length) {
87
131
  for (i = data.text.length - 1; i >= 0; i--) {
88
- // fixed the regex to parse email address correctly when email address has more than one @
89
- data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, _regexHandler).trim();
90
- if (data.address.length) {
91
- break;
132
+ // Security fix: Do not extract email addresses from quoted strings
133
+ if (!data.textWasQuoted[i]) {
134
+ // fixed the regex to parse email address correctly when email address has more than one @
135
+ data.text[i] = data.text[i].replace(/\s*\b[^@\s]+@[^\s]+\b\s*/, _regexHandler).trim();
136
+ if (data.address.length) {
137
+ break;
138
+ }
92
139
  }
93
140
  }
94
141
  }
@@ -180,11 +227,12 @@ class Tokenizer {
180
227
  * @return {Array} An array of operator|text tokens
181
228
  */
182
229
  tokenize() {
183
- let chr,
184
- list = [];
230
+ let list = [];
231
+
185
232
  for (let i = 0, len = this.str.length; i < len; i++) {
186
- chr = this.str.charAt(i);
187
- this.checkChar(chr);
233
+ let chr = this.str.charAt(i);
234
+ let nextChr = i < len - 1 ? this.str.charAt(i + 1) : null;
235
+ this.checkChar(chr, nextChr);
188
236
  }
189
237
 
190
238
  this.list.forEach(node => {
@@ -202,7 +250,7 @@ class Tokenizer {
202
250
  *
203
251
  * @param {String} chr Character from the address field
204
252
  */
205
- checkChar(chr) {
253
+ checkChar(chr, nextChr) {
206
254
  if (this.escaped) {
207
255
  // ignore next condition blocks
208
256
  } else if (chr === this.operatorExpecting) {
@@ -210,10 +258,16 @@ class Tokenizer {
210
258
  type: 'operator',
211
259
  value: chr
212
260
  };
261
+
262
+ if (nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr)) {
263
+ this.node.noBreak = true;
264
+ }
265
+
213
266
  this.list.push(this.node);
214
267
  this.node = null;
215
268
  this.operatorExpecting = '';
216
269
  this.escaped = false;
270
+
217
271
  return;
218
272
  } else if (!this.operatorExpecting && chr in this.operators) {
219
273
  this.node = {
package/tsconfig.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "lib": ["ES2022", "DOM"],
6
+ "moduleResolution": "bundler",
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "skipLibCheck": true,
10
+ "esModuleInterop": true,
11
+ "allowSyntheticDefaultImports": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "types": ["node"]
14
+ },
15
+ "include": ["test/type-check.test.ts"],
16
+ "exclude": ["node_modules"]
17
+ }