@dataloop-ai/components 0.20.127 → 0.20.129

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dataloop-ai/components",
3
- "version": "0.20.127",
3
+ "version": "0.20.129",
4
4
  "exports": {
5
5
  ".": "./index.ts",
6
6
  "./models": "./models.ts",
@@ -23,7 +23,7 @@
23
23
  "check-only": "if grep -E -H -r --exclude-dir=.git --exclude-dir=node_modules --exclude=*.json --exclude=*.yml '^(describe|it).only' .; then echo 'Found only in test files' && exit 1; fi"
24
24
  },
25
25
  "dependencies": {
26
- "@dataloop-ai/icons": "^3.1.15",
26
+ "@dataloop-ai/icons": "^3.1.22",
27
27
  "@types/flat": "^5.0.2",
28
28
  "@types/lodash": "^4.14.184",
29
29
  "@types/sortablejs": "^1.15.7",
@@ -37,6 +37,7 @@
37
37
  "sass": "^1.51.0",
38
38
  "sass-loader": "^12.6.0",
39
39
  "sortablejs": "^1.15.0",
40
+ "tokenizr": "^1.7.0",
40
41
  "uuid": "^8.3.2",
41
42
  "v-wave": "^1.5.0",
42
43
  "vanilla-jsoneditor": "^0.10.2",
@@ -181,6 +181,7 @@ import {
181
181
  } from '../../../../../hooks/use-suggestions'
182
182
  import { parseSmartQuery, stringifySmartQuery } from '../../../../../utils'
183
183
  import { StateManager, stateManager } from '../../../../../StateManager'
184
+ import { TokenType, tokenize } from '../../../../../utils/splitByQuotes'
184
185
 
185
186
  export default defineComponent({
186
187
  components: {
@@ -428,68 +429,63 @@ export default defineComponent({
428
429
  const value = '' + suggestion
429
430
  let stringValue = ''
430
431
  let caretPosition = 0
431
- if (searchQuery.value.length) {
432
- let queryLeftSide = searchQuery.value.substring(
433
- 0,
434
- caretAt.value
435
- )
436
- let queryRightSide = searchQuery.value.substring(caretAt.value)
437
432
 
433
+ const search = searchQuery.value ?? ''
434
+ const tokens = tokenize(search)
435
+ let leftTokenIndex = tokens.length
436
+ while(leftTokenIndex-- > 0) {
437
+ if (tokens[leftTokenIndex].pos < caretAt.value) {
438
+ break
439
+ }
440
+ }
441
+
442
+ if (leftTokenIndex < 0) {
443
+ stringValue = value + ' ' + search.replace(/^\S*\s*/, '')
444
+ caretPosition = value.length + 1
445
+ } else {
446
+ const token = tokens[leftTokenIndex]
447
+ const tokenLeftText = token.text.substring(0, caretAt.value - token.pos)
448
+ const tokenRightText = token.text.substring(caretAt.value - token.pos)
449
+
450
+ if (token.type === TokenType.WHITESPACE) {
451
+ // caret after space
452
+ token.text = ' ' + value + ' '
453
+ caretPosition = token.pos + 1 + value.length + 1
454
+ } else
438
455
  if (['AND', 'OR'].includes(value)) {
439
456
  // do not replace text if the value is AND or OR
440
- const leftover = queryLeftSide.match(/\S+$/)?.[0] || ''
441
- queryLeftSide =
442
- queryLeftSide.replace(/\S+$/, '').trimEnd() + ' '
443
- queryRightSide = leftover + queryRightSide
457
+ token.text = value + ' ' + token.text
458
+ caretPosition = token.pos + value.length + 1
444
459
  } else if (value.startsWith('.')) {
445
460
  // dot notation case
446
- const words = queryLeftSide.trimEnd().split('.')
461
+ const words = tokenLeftText.split('.')
447
462
  const lastWord = words.pop()
448
463
  if (!value.startsWith('.' + lastWord)) {
449
464
  words.push(lastWord)
450
465
  }
451
- queryLeftSide = words.join('.')
452
- } else if (
453
- queryLeftSide.endsWith(' ') &&
454
- (queryLeftSide.match(/'/g)?.length ?? 0) % 2 === 0
455
- ) {
456
- // caret after space: only replace multiple spaces on the left
457
- queryLeftSide = queryLeftSide.trimEnd() + ' '
458
- } else if (/\.\S+$/.test(queryLeftSide)) {
459
- // if there are dots in left side expression, suggestions have an operator
466
+ const text = words.join('.')
467
+ token.text = text + value + ' ' + tokenRightText
468
+ caretPosition = token.pos + text.length + value.length + 1
469
+ } else if (/\.\S+$/.test(tokenLeftText)) {
470
+ // if there are dots in left side expression...
460
471
  // looks like a bug in findSuggestions TODO find it - for now work around it here
461
- const leftover = queryRightSide.match(/^\S+/)?.[0] || ''
462
- queryLeftSide += leftover + ' '
463
- queryRightSide = queryRightSide
464
- .substring(leftover.length)
465
- .trimStart()
466
- } else if (queryRightSide.startsWith(' ')) {
467
- // this| situation: replace whatever is there on the left side with the value
468
- queryLeftSide = queryLeftSide.replace(/\S+$/, '')
469
- queryRightSide = queryRightSide.trimStart()
472
+ const leftover = tokenRightText.match(/^\S+/)?.[0] || ''
473
+ token.text = tokenLeftText + leftover + ' ' + value + ' ' +
474
+ tokenRightText.substring(leftover.length).trimStart()
475
+ caretPosition = token.pos + tokenLeftText.length +
476
+ leftover.length + 1 + value.length + 1
470
477
  } else {
478
+ // this| situation: replace whatever is there on the left side with the value
471
479
  // this|situation: replace whatever is there on both sides with the value
472
- if (
473
- /^[^']+((?<!\\)'([^']|(?<=\\)')*(?<!\\)'[^']+)*(?<!\\)'([^']+\\')*[^']*((?<!\\)')?$/.test(
474
- queryLeftSide
475
- )
476
- ) {
477
- queryLeftSide = queryLeftSide.replace(
478
- /(?<!\\)'([^']+\\')*[^']*((?<!\\)')?$/,
479
- ''
480
- )
481
- } else {
482
- queryLeftSide = queryLeftSide.replace(/[^'\s]+$/, '')
480
+ const newValue = token.type === TokenType.COMMA ? ', ' + value : value
481
+ token.text = newValue
482
+ caretPosition = token.pos + newValue.length
483
+ if (tokens[leftTokenIndex + 1]?.type !== TokenType.WHITESPACE) {
484
+ token.text += ' '
485
+ caretPosition += 1
483
486
  }
484
- queryRightSide =
485
- removeLeadingExpression(queryRightSide).trimStart()
486
487
  }
487
-
488
- stringValue = queryLeftSide + value + ' ' + queryRightSide
489
- caretPosition = stringValue.length - queryRightSide.length
490
- } else {
491
- stringValue = value + ' '
492
- caretPosition = stringValue.length
488
+ stringValue = tokens.map(token => token.text).join('')
493
489
  }
494
490
 
495
491
  setInputValue(stringValue)
@@ -1,3 +1,4 @@
1
+ import { tokenize } from '../../../../../utils/splitByQuotes'
1
2
  import { SyntaxColorSchema } from '../types'
2
3
 
3
4
  const SPAN_STYLES = `overflow: hidden;
@@ -91,7 +92,7 @@ function restoreSelection(
91
92
  }
92
93
 
93
94
  function renderText(text: string, colorSchema: SyntaxColorSchema) {
94
- const words = text?.split(/(\s+)/)
95
+ const words = tokenize(text ?? '').map(token => token.text)
95
96
  const output = words?.map((word) => {
96
97
  if (colorSchema) {
97
98
  if (colorSchema.keywords.values.includes(word)) {
@@ -305,6 +305,20 @@ export default defineComponent({
305
305
  type: Number,
306
306
  default: 100
307
307
  },
308
+ /**
309
+ * Disable child checkbox when parent is selected
310
+ */
311
+ disableChildCheckbox: {
312
+ type: Boolean,
313
+ default: false
314
+ },
315
+ /**
316
+ * Tooltip text for disabled child checkbox
317
+ */
318
+ childDisabledCheckboxTooltip: {
319
+ type: String,
320
+ default: 'Cannot unselect child when parent is selected'
321
+ },
308
322
  ...useTableActionsProps,
309
323
  ...commonVirtScrollProps,
310
324
  ...useTableRowExpandProps,
@@ -532,12 +546,14 @@ export default defineComponent({
532
546
  row: DlTableRow,
533
547
  index: number,
534
548
  children: DlTableRow[] = [],
535
- level: number = 1
549
+ level: number = 1,
550
+ parentSelected: boolean = false
536
551
  ) => {
537
552
  const currentSlots = {
538
553
  default: () => children,
539
554
  ...computedCellSlots.value
540
555
  }
556
+
541
557
  return renderComponent(vue2h.value, DlTrTreeView, {
542
558
  row,
543
559
  rowIndex: index,
@@ -580,6 +596,10 @@ export default defineComponent({
580
596
  customIconExpandedRow: props.customIconExpandedRow,
581
597
  customIconCompressedRow: props.customIconCompressedRow,
582
598
  chevronIconColor: props.chevronIconColor,
599
+ disableChildCheckbox: props.disableChildCheckbox,
600
+ childDisabledCheckboxTooltip:
601
+ props.childDisabledCheckboxTooltip,
602
+ parentSelected,
583
603
  'onUpdate:modelValue': (adding: boolean, evt: Event) => {
584
604
  updateSelectionHierarchy(adding, evt, row)
585
605
  },
@@ -623,11 +643,14 @@ export default defineComponent({
623
643
  const renderTr = (
624
644
  row: DlTableRow,
625
645
  index: number,
626
- level: number = 1
646
+ level: number = 1,
647
+ parentSelected: boolean = false
627
648
  ) => {
628
649
  const children = []
650
+ const isCurrentRowSelected =
651
+ isRowSelected(props.rowKey, getRowKey.value(row)) === true
629
652
 
630
- children.push(renderDlTrTree(row, index, [], level))
653
+ children.push(renderDlTrTree(row, index, [], level, parentSelected))
631
654
 
632
655
  const tbodyEls: VNode[] = []
633
656
 
@@ -645,7 +668,7 @@ export default defineComponent({
645
668
  'data-level': level,
646
669
  class: 'nested-tbody'
647
670
  },
648
- renderTr(childRow, i, level)
671
+ renderTr(childRow, i, level, isCurrentRowSelected)
649
672
  ) as VNode
650
673
  )
651
674
  })
@@ -20,12 +20,18 @@
20
20
  </td>
21
21
  <td v-if="hasSelectionMode" class="dl-table--col-auto-width">
22
22
  <slot name="body-selection" v-bind="bindBodySelection">
23
+ <dl-tooltip
24
+ v-if="isCheckboxDisabled && childDisabledCheckboxTooltip"
25
+ >
26
+ {{ childDisabledCheckboxTooltip }}
27
+ </dl-tooltip>
23
28
  <DlCheckbox
24
29
  :color="color"
25
30
  :model-value="modelValue"
26
31
  :indeterminate-value="true"
27
32
  :false-value="false"
28
33
  :true-value="true"
34
+ :disabled="isCheckboxDisabled"
29
35
  @update:model-value="
30
36
  (adding, evt) => emitUpdateModelValue(adding, evt)
31
37
  "
@@ -84,12 +90,14 @@ import {
84
90
  ref,
85
91
  toRefs,
86
92
  watch,
87
- getCurrentInstance
93
+ getCurrentInstance,
94
+ computed
88
95
  } from 'vue-demi'
89
96
  import DlTrTree from '../components/DlTrTree.vue'
90
97
  import DlTdTree from '../components/DlTdTree.vue'
91
98
  import DlIcon from '../../../essential/DlIcon/DlIcon.vue'
92
99
  import DlCheckbox from '../../../essential/DlCheckbox/DlCheckbox.vue'
100
+ import DlTooltip from '../../../shared/DlTooltip/DlTooltip.vue'
93
101
  import { getRowKey } from '../utils/getRowKey'
94
102
  import { DlTableRow } from '../../DlTable/types'
95
103
  import { setTrPadding } from '../utils/trSpacing'
@@ -101,7 +109,8 @@ export default defineComponent({
101
109
  DlTrTree,
102
110
  DlTdTree,
103
111
  DlIcon,
104
- DlCheckbox
112
+ DlCheckbox,
113
+ DlTooltip
105
114
  },
106
115
  props: {
107
116
  row: {
@@ -187,6 +196,27 @@ export default defineComponent({
187
196
  isRowHighlighted: {
188
197
  type: Boolean,
189
198
  default: false
199
+ },
200
+ /**
201
+ * Disable child checkbox when parent is selected
202
+ */
203
+ disableChildCheckbox: {
204
+ type: Boolean,
205
+ default: false
206
+ },
207
+ /**
208
+ * Tooltip text for disabled child checkbox
209
+ */
210
+ childDisabledCheckboxTooltip: {
211
+ type: String,
212
+ default: 'Cannot unselect child when parent is selected'
213
+ },
214
+ /**
215
+ * Whether the parent row is selected
216
+ */
217
+ parentSelected: {
218
+ type: Boolean,
219
+ default: false
190
220
  }
191
221
  },
192
222
  emits: [
@@ -206,6 +236,18 @@ export default defineComponent({
206
236
 
207
237
  const vm = getCurrentInstance()
208
238
 
239
+ const isCheckboxDisabled = computed(() => {
240
+ if (!props.disableChildCheckbox) {
241
+ return false
242
+ }
243
+
244
+ if (props.level === 1) {
245
+ return false
246
+ }
247
+
248
+ return props.parentSelected
249
+ })
250
+
209
251
  watch(
210
252
  row,
211
253
  () => {
@@ -364,7 +406,8 @@ export default defineComponent({
364
406
  getExpandedvisibleChildren,
365
407
  updateExpandedvisibleChildren,
366
408
  onRowHoverStart,
367
- onRowHoverEnd
409
+ onRowHoverEnd,
410
+ isCheckboxDisabled
368
411
  }
369
412
  }
370
413
  })
@@ -86,6 +86,14 @@
86
86
  :model-value="resizableState"
87
87
  @update:model-value="updateResizableState"
88
88
  />
89
+ <dl-switch
90
+ left-label="Disable Child Checkbox"
91
+ value="disableChildCheckbox"
92
+ :model-value="disableChildCheckboxState"
93
+ @update:model-value="
94
+ updateDisableChildCheckboxState
95
+ "
96
+ />
89
97
  </div>
90
98
  </div>
91
99
  </div>
@@ -109,6 +117,8 @@
109
117
  style="height: 500px"
110
118
  :rows-per-page-options="rowsPerPageOptions"
111
119
  highlighted-row="Frozen Yogurt"
120
+ :disable-child-checkbox="disableChildCheckbox"
121
+ child-disabled-checkbox-tooltip="Child checkbox is disabled (parent is selected)"
112
122
  @row-click="onRowClick"
113
123
  @th-click="log"
114
124
  @selected-items="selectedItems"
@@ -583,6 +593,9 @@ export default defineComponent({
583
593
 
584
594
  const nextPageNumber = ref(2)
585
595
 
596
+ const disableChildCheckbox = ref(false)
597
+ const disableChildCheckboxState = ref([])
598
+
586
599
  let allRows: DlTableRow[] = []
587
600
  for (let i = 0; i < 100; i++) {
588
601
  allRows = allRows.concat(
@@ -732,7 +745,9 @@ export default defineComponent({
732
745
  isFirstPage,
733
746
  onRowClick,
734
747
  rows2,
735
- columns2
748
+ columns2,
749
+ disableChildCheckbox,
750
+ disableChildCheckboxState
736
751
  }
737
752
  },
738
753
 
@@ -765,6 +780,11 @@ export default defineComponent({
765
780
 
766
781
  this.resizable = val.length !== 0
767
782
  },
783
+ updateDisableChildCheckboxState(val: boolean[]): void {
784
+ this.disableChildCheckboxState = val
785
+
786
+ this.disableChildCheckbox = val.length !== 0
787
+ },
768
788
  log(...args: any[]) {
769
789
  console.log(...args)
770
790
  }
@@ -1,5 +1,6 @@
1
1
  import { Ref, ref } from 'vue-demi'
2
- import { splitByQuotes } from '../utils/splitByQuotes'
2
+ import { splitByQuotes, tokenize, TokenType } from '../utils/splitByQuotes'
3
+ import { Token } from 'tokenizr'
3
4
  import { flatten } from 'flat'
4
5
  import { isObject } from 'lodash'
5
6
 
@@ -192,12 +193,56 @@ export const useSuggestions = (
192
193
  }
193
194
 
194
195
  const findSuggestions = (input: string) => {
195
- input = input.replace(/\s+/g, ' ').trimStart()
196
- localSuggestions = sortedSuggestions
196
+ const tokens = tokenize(input)
197
+
198
+ let fieldToken: Token | null = null
199
+ let operatorToken: Token | null = null
200
+ let valueToken: Token | null = null
201
+ let keywordToken: Token | null = null
202
+
203
+ let i = tokens.length -1
204
+ let whitespace = false
205
+ while (i > -1) {
206
+ const token = tokens[i]
207
+ switch (token.type) {
208
+ case TokenType.WHITESPACE:
209
+ whitespace = true
210
+ break
211
+ case TokenType.LOGICAL:
212
+ keywordToken = token
213
+ break
214
+ case TokenType.BOOLEAN:
215
+ case TokenType.COMMA:
216
+ case TokenType.DATETIME:
217
+ case TokenType.NUMBER:
218
+ case TokenType.STRING:
219
+ case TokenType.PARTIAL_VALUE:
220
+ if (!valueToken) {
221
+ valueToken = token
222
+ }
223
+ break
224
+ case TokenType.OPERATOR:
225
+ // quirk: mapWordsToExpression would only set operator if followed by a whitespace
226
+ if (whitespace || valueToken) {
227
+ operatorToken = token
228
+ }
229
+ break
230
+ case TokenType.FIELD:
231
+ fieldToken = token
232
+ i = 0
233
+ break
234
+ }
235
+ i--
236
+ }
197
237
 
198
- const words = splitByQuotes(input, space)
199
- const mergedWords = mergeWords(words)
200
- const expressions = mapWordsToExpressions(mergedWords)
238
+ const expressions: Expression[] = [{
239
+ field: fieldToken?.text,
240
+ operator: operatorToken?.text,
241
+ value: valueToken?.type === TokenType.COMMA ? '' : valueToken?.text,
242
+ keyword: keywordToken?.text
243
+ }]
244
+
245
+ localSuggestions = sortedSuggestions
201
246
 
202
247
  for (const { field, operator, value, keyword } of expressions) {
203
248
  let matchedField: Suggestion | null = null
@@ -228,7 +273,9 @@ export const useSuggestions = (
228
273
 
229
274
  if (
230
275
  !matchedField ||
231
- (!isNextCharSpace(input, matchedField) &&
276
+ (
277
+ !operator &&
278
+ insensitive(input).endsWith(insensitive(matchedField)) &&
232
279
  fieldSeparated.length === 1)
233
280
  ) {
234
281
  continue
@@ -328,7 +375,10 @@ export const useSuggestions = (
328
375
  continue
329
376
  }
330
377
 
331
- if (!matchedOperator || !isNextCharSpace(input, matchedOperator)) {
378
+ if (!matchedOperator || (
379
+ !value &&
380
+ !isNextCharSpace(input, matchedOperator)
381
+ )) {
332
382
  continue
333
383
  }
334
384
 
@@ -1,41 +1,112 @@
1
- export function splitByQuotes(input: string, split: string) {
2
- const pattern = '([\\s\\S]*?)(e)?(?:(o)|(c)|(t)|(sp)|$)'
3
- .replace('sp', split)
4
- .replace('o', '[\\(\\{\\[]')
5
- .replace('c', '[\\)\\}\\]]')
6
- .replace('t', '(?<!\\\\)[\'"]')
7
- .replace('e', '[\\\\]')
8
- const r = new RegExp(pattern, 'gi')
9
- const stack: string[] = []
10
- let buffer: string[] = []
11
- const results: string[] = []
12
- input.replace(r, ($0, $1, $e, $o, $c, $t, $s, i): any => {
13
- if ($e) {
14
- buffer.push($1, $s || $o || $c || $t)
15
- return
16
- } else if ($o) {
17
- if (!stack.includes("'") && !stack.includes('"')) {
18
- stack.push($o)
19
- }
20
- } else if ($c) {
21
- if (!stack.includes("'") && !stack.includes('"')) {
22
- stack.pop()
23
- }
24
- } else if ($t) {
25
- const otherQuote = $t === '"' ? "'" : '"'
26
- if (!stack.includes(otherQuote)) {
27
- if (stack[stack.length - 1] !== $t) stack.push($t)
28
- else stack.pop()
29
- }
1
+ import Tokenizr from "tokenizr"
2
+
3
+ export enum TokenType {
4
+ NUMBER = 'number',
5
+ DATETIME = 'datetime',
6
+ OPERATOR = 'operator',
7
+ LOGICAL = 'logical',
8
+ BOOLEAN = 'boolean',
9
+ FIELD = 'field',
10
+ COMMA = 'comma',
11
+ STRING = 'string',
12
+ PARTIAL_VALUE = 'partial-string',
13
+ WHITESPACE = 'whitespace'
14
+ }
15
+
16
+ enum Tags {
17
+ HAD_FIELD = 'had-field',
18
+ HAD_VALUE = 'had-value'
19
+ }
20
+
21
+ let tokenizer = new Tokenizr()
22
+
23
+ tokenizer.rule(/[+-]?[0-9\.]+/, (ctx, match) => {
24
+ ctx.accept(TokenType.NUMBER, parseFloat(match[0])).tag(Tags.HAD_VALUE)
25
+ })
26
+
27
+ tokenizer.rule(/\((\d{2}\/\d{2}\/\d{4}[\)']?\s?|\s?DD\/MM\/YYYY)\s?(\d{2}:\d{2}:\d{2}|\s?HH:mm:ss)?\)/, (ctx, match) => {
28
+ ctx.accept(TokenType.DATETIME, parseFloat(match[0])).tag(Tags.HAD_VALUE)
29
+ })
30
+
31
+ ;[
32
+ /<=?/, />=?/, /!=?/, /=/,
33
+ /in?(?![^\s'"])/i,
34
+ /n(o(t(-(in?)?)?)?)?(?![^\s'"])/i,
35
+ /e(x(i(s(ts?)?)?)?)?(?!\S)/i,
36
+ /d(o(e(s(n(t(-(e(x(i(st?)?)?)?)?)?)?)?)?)?)?(?!\S)/i
37
+ ].forEach(re => tokenizer.rule(re, (ctx, match) => {
38
+ if (!ctx.tagged(Tags.HAD_FIELD) && /^[a-z]/i.test(match[0])) {
39
+ ctx.accept(TokenType.FIELD).tag(Tags.HAD_FIELD)
40
+ } else {
41
+ ctx.accept(TokenType.OPERATOR, match[0].toUpperCase())
42
+ }
43
+ }))
44
+
45
+ tokenizer.rule(/[a-z][a-z\.\d\-_]*/i, (ctx, match) => {
46
+ const upper = match[0].toUpperCase()
47
+ if (ctx.tagged(Tags.HAD_VALUE)) {
48
+ // we just had a value - it would be followed by AND or OR
49
+ ctx.untag(Tags.HAD_FIELD).untag(Tags.HAD_VALUE).accept(TokenType.LOGICAL, upper)
50
+ } else if (ctx.tagged(Tags.HAD_FIELD)) {
51
+ // we had a field but no value yet - this must be either a boolean or an unquoted string
52
+ if (['TRUE', 'FALSE'].includes(upper)) {
53
+ ctx.accept(TokenType.BOOLEAN, upper === 'TRUE').tag(Tags.HAD_VALUE)
30
54
  } else {
31
- if ($s ? !stack.length : !$1) {
32
- buffer.push($1)
33
- results.push(buffer.join(''))
34
- buffer = []
35
- return
36
- }
55
+ ctx.accept(TokenType.PARTIAL_VALUE).tag(Tags.HAD_VALUE)
37
56
  }
38
- buffer.push($0)
39
- })
40
- return results
57
+ } else {
58
+ ctx.accept(TokenType.FIELD).tag(Tags.HAD_FIELD)
59
+ }
60
+ })
61
+
62
+ tokenizer.rule(/,/, (ctx) => {
63
+ // untag HAD_VALUE because a comma cannot be followed by AND or OR - but by another value
64
+ ctx.untag(Tags.HAD_VALUE).accept(TokenType.COMMA)
65
+ })
66
+
67
+ tokenizer.rule(/(?<!\\)"(.*?)(?<!\\)"/, (ctx, match) => {
68
+ ctx.accept(TokenType.STRING, match[1].replace(/\\"/g, '"')).tag(Tags.HAD_VALUE)
69
+ })
70
+
71
+ tokenizer.rule(/(?<!\\)'(.*?)(?<!\\)'/, (ctx, match) => {
72
+ ctx.accept(TokenType.STRING, match[1].replace(/\\'/g, "'")).tag(Tags.HAD_VALUE)
73
+ })
74
+
75
+ tokenizer.rule(/(?<!\\)['"](.*)/, (ctx, match) => {
76
+ // partial string
77
+ ctx.accept(TokenType.PARTIAL_VALUE, match[1].replace(/\\'/g, "'")).tag(Tags.HAD_VALUE)
78
+ })
79
+
80
+ tokenizer.rule(/\s+/, (ctx) => {
81
+ ctx.accept(TokenType.WHITESPACE)
82
+ })
83
+
84
+ tokenizer.rule(/.+/, (ctx, match) => {
85
+ // unrecognized token
86
+ ctx.accept(TokenType.WHITESPACE, match[0].replaceAll(/./g, ' '))
87
+ })
88
+
89
+ export function tokenize(input: string) {
90
+ tokenizer.reset()
91
+ tokenizer.input(input)
92
+ return tokenizer.tokens()
93
+ }
94
+
95
+ export function splitByQuotes(input: string, ignore?: string) {
96
+ const parts = tokenize(input)
97
+ .filter(token => token.type !== 'whitespace')
98
+ .map(token => token.text)
99
+ .map((text, index, array) => {
100
+ if(array[index + 1] === ',') {
101
+ return text + ','
102
+ } else {
103
+ return text === ',' ? '' : text
104
+ }
105
+ })
106
+ .filter(text => text !== '')
107
+
108
+ if (/\s$/.test(input)) {
109
+ parts.push('')
110
+ }
111
+ return parts
41
112
  }