igniteui-angular 22.2.0-rc.0 → 22.2.0-rc.2

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.
Files changed (111) hide show
  1. package/README.md +1 -1
  2. package/button-group/README.md +42 -9
  3. package/calendar/README.md +30 -20
  4. package/card/README.md +1 -1
  5. package/fesm2022/igniteui-angular-accordion.mjs +7 -7
  6. package/fesm2022/igniteui-angular-action-strip.mjs +11 -22
  7. package/fesm2022/igniteui-angular-action-strip.mjs.map +1 -1
  8. package/fesm2022/igniteui-angular-avatar.mjs +7 -7
  9. package/fesm2022/igniteui-angular-badge.mjs +7 -7
  10. package/fesm2022/igniteui-angular-banner.mjs +10 -10
  11. package/fesm2022/igniteui-angular-bottom-nav.mjs +22 -22
  12. package/fesm2022/igniteui-angular-button-group.mjs +32 -35
  13. package/fesm2022/igniteui-angular-button-group.mjs.map +1 -1
  14. package/fesm2022/igniteui-angular-calendar.mjs +132 -180
  15. package/fesm2022/igniteui-angular-calendar.mjs.map +1 -1
  16. package/fesm2022/igniteui-angular-card.mjs +47 -58
  17. package/fesm2022/igniteui-angular-card.mjs.map +1 -1
  18. package/fesm2022/igniteui-angular-carousel.mjs +22 -22
  19. package/fesm2022/igniteui-angular-chat-extras.mjs +6 -6
  20. package/fesm2022/igniteui-angular-chat.mjs +12 -12
  21. package/fesm2022/igniteui-angular-checkbox.mjs +7 -7
  22. package/fesm2022/igniteui-angular-chips.mjs +10 -10
  23. package/fesm2022/igniteui-angular-combo.mjs +69 -67
  24. package/fesm2022/igniteui-angular-combo.mjs.map +1 -1
  25. package/fesm2022/igniteui-angular-core.mjs +139 -92
  26. package/fesm2022/igniteui-angular-core.mjs.map +1 -1
  27. package/fesm2022/igniteui-angular-date-picker.mjs +53 -70
  28. package/fesm2022/igniteui-angular-date-picker.mjs.map +1 -1
  29. package/fesm2022/igniteui-angular-dialog.mjs +13 -13
  30. package/fesm2022/igniteui-angular-directives.mjs +194 -194
  31. package/fesm2022/igniteui-angular-drop-down.mjs +29 -29
  32. package/fesm2022/igniteui-angular-expansion-panel.mjs +28 -28
  33. package/fesm2022/igniteui-angular-grids-core.mjs +737 -661
  34. package/fesm2022/igniteui-angular-grids-core.mjs.map +1 -1
  35. package/fesm2022/igniteui-angular-grids-grid.mjs +49 -49
  36. package/fesm2022/igniteui-angular-grids-hierarchical-grid.mjs +37 -37
  37. package/fesm2022/igniteui-angular-grids-lite.mjs +25 -17
  38. package/fesm2022/igniteui-angular-grids-lite.mjs.map +1 -1
  39. package/fesm2022/igniteui-angular-grids-pivot-grid.mjs +80 -80
  40. package/fesm2022/igniteui-angular-grids-pivot-grid.mjs.map +1 -1
  41. package/fesm2022/igniteui-angular-grids-tree-grid.mjs +55 -55
  42. package/fesm2022/igniteui-angular-icon.mjs +10 -10
  43. package/fesm2022/igniteui-angular-input-group.mjs +59 -55
  44. package/fesm2022/igniteui-angular-input-group.mjs.map +1 -1
  45. package/fesm2022/igniteui-angular-list.mjs +40 -40
  46. package/fesm2022/igniteui-angular-navbar.mjs +13 -13
  47. package/fesm2022/igniteui-angular-navigation-drawer.mjs +43 -38
  48. package/fesm2022/igniteui-angular-navigation-drawer.mjs.map +1 -1
  49. package/fesm2022/igniteui-angular-paginator.mjs +19 -19
  50. package/fesm2022/igniteui-angular-progressbar.mjs +19 -19
  51. package/fesm2022/igniteui-angular-query-builder.mjs +22 -22
  52. package/fesm2022/igniteui-angular-radio.mjs +25 -21
  53. package/fesm2022/igniteui-angular-radio.mjs.map +1 -1
  54. package/fesm2022/igniteui-angular-select.mjs +29 -33
  55. package/fesm2022/igniteui-angular-select.mjs.map +1 -1
  56. package/fesm2022/igniteui-angular-simple-combo.mjs +9 -9
  57. package/fesm2022/igniteui-angular-simple-combo.mjs.map +1 -1
  58. package/fesm2022/igniteui-angular-slider.mjs +28 -28
  59. package/fesm2022/igniteui-angular-snackbar.mjs +7 -7
  60. package/fesm2022/igniteui-angular-splitter.mjs +13 -13
  61. package/fesm2022/igniteui-angular-stepper.mjs +34 -34
  62. package/fesm2022/igniteui-angular-switch.mjs +7 -7
  63. package/fesm2022/igniteui-angular-tabs.mjs +34 -34
  64. package/fesm2022/igniteui-angular-time-picker.mjs +26 -36
  65. package/fesm2022/igniteui-angular-time-picker.mjs.map +1 -1
  66. package/fesm2022/igniteui-angular-toast.mjs +7 -7
  67. package/fesm2022/igniteui-angular-tree.mjs +28 -28
  68. package/fesm2022/igniteui-angular-virtual-scroll.mjs +497 -145
  69. package/fesm2022/igniteui-angular-virtual-scroll.mjs.map +1 -1
  70. package/migrations/common/UpdateChanges.d.ts +56 -0
  71. package/migrations/common/UpdateChanges.js +366 -38
  72. package/migrations/common/UpdateChanges.spec.js +829 -0
  73. package/migrations/migration-collection.json +1 -1
  74. package/migrations/update-22_2_0/index.js +145 -0
  75. package/migrations/update-22_2_0/index.spec.js +206 -0
  76. package/navigation-drawer/README.md +1 -1
  77. package/package.json +3 -3
  78. package/schematics/tsconfig.tsbuildinfo +1 -1
  79. package/skills/igniteui-angular-components/SKILL.md +9 -5
  80. package/skills/igniteui-angular-components/references/form-controls.md +1 -1
  81. package/skills/igniteui-angular-components/references/mcp-setup.md +14 -2
  82. package/skills/igniteui-angular-figma-to-app/SKILL.md +112 -525
  83. package/skills/igniteui-angular-figma-to-app/references/asset-extraction.md +49 -75
  84. package/skills/igniteui-angular-figma-to-app/references/design-provenance.md +201 -0
  85. package/skills/igniteui-angular-figma-to-app/references/design-token-bridge.md +175 -52
  86. package/skills/igniteui-angular-figma-to-app/references/figma-component-map.md +153 -99
  87. package/skills/igniteui-angular-figma-to-app/references/figma-exploration.md +226 -0
  88. package/skills/igniteui-angular-figma-to-app/references/mcp-setup.md +71 -105
  89. package/skills/igniteui-angular-figma-to-app/references/project-setup.md +63 -0
  90. package/skills/igniteui-angular-figma-to-app/references/theme-generation.md +120 -0
  91. package/skills/igniteui-angular-figma-to-app/references/validation-patterns.md +48 -50
  92. package/skills/igniteui-angular-generate-from-image-design/SKILL.md +11 -7
  93. package/skills/igniteui-angular-grids/SKILL.md +7 -3
  94. package/skills/igniteui-angular-grids/references/editing.md +1 -2
  95. package/skills/igniteui-angular-grids/references/grid-migration.md +1 -1
  96. package/skills/igniteui-angular-theming/SKILL.md +9 -5
  97. package/skills/igniteui-angular-theming/references/mcp-setup.md +12 -2
  98. package/types/igniteui-angular-button-group.d.ts +49 -34
  99. package/types/igniteui-angular-calendar.d.ts +33 -50
  100. package/types/igniteui-angular-card.d.ts +12 -17
  101. package/types/igniteui-angular-combo.d.ts +6 -0
  102. package/types/igniteui-angular-core.d.ts +25 -8
  103. package/types/igniteui-angular-grids-core.d.ts +48 -4
  104. package/types/igniteui-angular-grids-lite.d.ts +5 -1
  105. package/types/igniteui-angular-grids-pivot-grid.d.ts +1 -1
  106. package/types/igniteui-angular-input-group.d.ts +17 -3
  107. package/types/igniteui-angular-navigation-drawer.d.ts +1 -0
  108. package/types/igniteui-angular-radio.d.ts +5 -0
  109. package/types/igniteui-angular-time-picker.d.ts +0 -1
  110. package/types/igniteui-angular-virtual-scroll.d.ts +41 -13
  111. package/virtual-scroll/README.md +32 -1
@@ -47,6 +47,9 @@ const tsUtils_1 = require("./tsUtils");
47
47
  const util_1 = require("./util");
48
48
  const project_service_container_1 = require("./project-service-container");
49
49
  const TSCONFIG_PATH = 'tsconfig.json';
50
+ const URL_TOKEN = 'url(';
51
+ /** Dependency and build output folders never searched for stylesheets. */
52
+ const SKIPPED_STYLE_DIRS = ['node_modules', 'dist', '.angular', '.git'];
50
53
  var InputPropertyType;
51
54
  (function (InputPropertyType) {
52
55
  InputPropertyType["EVAL"] = "eval";
@@ -100,17 +103,25 @@ class UpdateChanges {
100
103
  /** Sass (both .scss and .sass) files in the project being updated. */
101
104
  get sassFiles() {
102
105
  if (!this._sassFiles.length) {
103
- // files can be outside the app prefix, so start from sourceRoot
106
+ // stylesheets can sit anywhere in the workspace, not only under sourceRoot
104
107
  // also ignore schematics `styleext` as Sass can be used regardless
105
- const sourceDirs = (0, util_1.getProjects)(this.workspace).map(x => x.sourceRoot).filter(x => x);
106
- this.sourceDirsVisitor((fulPath, entry) => {
107
- if (fulPath.endsWith('.scss') || fulPath.endsWith('.sass')) {
108
- this._sassFiles.push(entry.path);
109
- }
110
- }, sourceDirs);
108
+ this.collectSassFiles(this.host.getDir('/'), this._sassFiles);
111
109
  }
112
110
  return this._sassFiles;
113
111
  }
112
+ /** Collects the Sass files under a folder, skipping dependency and build output folders. */
113
+ collectSassFiles(dir, files) {
114
+ for (const name of dir.subfiles) {
115
+ if (name.endsWith('.scss') || name.endsWith('.sass')) {
116
+ files.push(dir.file(name).path);
117
+ }
118
+ }
119
+ for (const name of dir.subdirs) {
120
+ if (!SKIPPED_STYLE_DIRS.includes(name)) {
121
+ this.collectSassFiles(dir.dir(name), files);
122
+ }
123
+ }
124
+ }
114
125
  get service() {
115
126
  if (!this._service) {
116
127
  this._service = (0, tsUtils_1.getLanguageService)(this.tsFiles, this.host);
@@ -361,52 +372,361 @@ class UpdateChanges {
361
372
  }
362
373
  }
363
374
  updateThemeProps(entryPath) {
364
- var _a;
365
375
  let fileContent = this.host.read(entryPath).toString();
366
376
  let overwrite = false;
367
377
  for (const change of this.themeChanges.changes) {
368
378
  if (change.type !== schema_1.ThemeType.Property) {
369
379
  continue;
370
380
  }
371
- if (fileContent.indexOf(change.owner) !== -1) {
372
- /** owner-func:( * ); */
373
- const searchPattern = String.raw `${change.owner}\([\s\S]+?\);`;
374
- const matches = fileContent.match(new RegExp(searchPattern, 'g'));
375
- if (!matches) {
381
+ if (fileContent.indexOf(change.owner) === -1) {
382
+ continue;
383
+ }
384
+ /** owner-func:( * ) */
385
+ const calls = this.findFunctionCalls(fileContent, change.owner, entryPath.endsWith('.sass'));
386
+ // rewrite back to front so the collected indices stay valid
387
+ for (const call of calls.reverse()) {
388
+ const rawBody = fileContent.substring(call.bodyStart, call.bodyEnd);
389
+ if (rawBody.indexOf(change.name) === -1) {
376
390
  continue;
377
391
  }
378
- for (const match of matches) {
379
- if (match.indexOf(change.name) !== -1) {
380
- const name = change.name.replace('$', '\\$');
381
- const replaceWith = (_a = change.replaceWith) === null || _a === void 0 ? void 0 : _a.replace('$', '\\$');
382
- const reg = new RegExp(String.raw `^\s*${name}:`);
383
- const existing = new RegExp(String.raw `${replaceWith}:`);
384
- const opening = `${change.owner}(`;
385
- const closing = /\s*\);$/.exec(match).pop();
386
- const body = match.substr(opening.length, match.length - opening.length - closing.length);
387
- let params = this.splitFunctionProps(body);
388
- params = params.reduce((arr, param) => {
389
- if (reg.test(param)) {
390
- const duplicate = !!replaceWith && arr.some(p => existing.test(p));
391
- if (!change.remove && !duplicate) {
392
- arr.push(param.replace(change.name, change.replaceWith));
393
- }
394
- }
395
- else {
396
- arr.push(param);
397
- }
398
- return arr;
399
- }, []);
400
- fileContent = fileContent.replace(match, opening + params.join(',') + closing);
401
- overwrite = true;
392
+ const name = (0, util_1.escapeRegExp)(change.name);
393
+ const replaceWith = change.replaceWith ? (0, util_1.escapeRegExp)(change.replaceWith) : undefined;
394
+ // both are matched at the argument's code start; Sass allows whitespace before the colon
395
+ const reg = new RegExp(String.raw `^${name}\s*:`);
396
+ const existing = new RegExp(String.raw `^${replaceWith}\s*:`);
397
+ // keep whatever sits in front of the closing bracket so the formatting is preserved
398
+ const trailing = /\s*$/.exec(rawBody).pop();
399
+ const body = rawBody.substring(0, rawBody.length - trailing.length);
400
+ const eol = fileContent.includes('\r\n') ? '\r\n' : '\n';
401
+ const params = this.splitFunctionProps(body);
402
+ const codeStarts = params.map(param => this.leadingCommentsEnd(param));
403
+ // renaming onto a name the call already passes would make it a duplicate argument
404
+ const duplicate = !!replaceWith && params.some((param, i) => existing.test(param.substring(codeStarts[i])));
405
+ // comments in front of a removed argument usually describe the previous one, so they are kept
406
+ let carried = '';
407
+ const kept = params.reduce((arr, param, i) => {
408
+ const codeStart = codeStarts[i];
409
+ if (reg.test(param.substring(codeStart))) {
410
+ if (!change.remove && !duplicate) {
411
+ // the name sits at the code start, so renaming must not search the comments
412
+ arr.push(carried + this.commentBreak(carried, param, eol) + param.substring(0, codeStart)
413
+ + change.replaceWith + param.substring(codeStart + change.name.length));
414
+ carried = '';
415
+ }
416
+ else {
417
+ carried += param.substring(0, codeStart).trimEnd();
418
+ }
402
419
  }
403
- }
420
+ else {
421
+ arr.push(carried + this.commentBreak(carried, param, eol) + param);
422
+ carried = '';
423
+ }
424
+ return arr;
425
+ }, []);
426
+ fileContent = fileContent.substring(0, call.bodyStart)
427
+ + kept.join(',') + carried + this.commentBreak(carried, trailing, eol)
428
+ + trailing
429
+ + fileContent.substring(call.bodyEnd);
430
+ overwrite = true;
404
431
  }
405
432
  }
406
433
  if (overwrite) {
407
434
  this.host.overwrite(entryPath, fileContent);
408
435
  }
409
436
  }
437
+ /**
438
+ * Returns the argument list boundaries of every top-level `owner(...)` call in the content.
439
+ * Strings, comments and `url()` tokens are scanned over, so an `owner(` that is only mentioned
440
+ * in one of those is not taken for a call, and anything the stylesheet declares itself is left
441
+ * alone. The brackets are tracked too, so a call nested in another one -
442
+ * `@include scrollbar(scrollbar-theme($sb-size: 6px))` - reports its own closing bracket
443
+ * rather than the one of the call surrounding it.
444
+ *
445
+ * An owner is matched anywhere in a name, because migrations rely on it: `circular-theme` is
446
+ * how `progress-circular-theme` is addressed, and `theme` covers every `*-theme` mixin.
447
+ */
448
+ findFunctionCalls(content, owner, indented) {
449
+ const calls = [];
450
+ const opening = `${owner}(`;
451
+ const code = this.sassCode(content);
452
+ // Earlier changes can move both calls and scope boundaries, so collect them together.
453
+ const scopes = this.declarationScopes(content, code, indented);
454
+ let brackets = 0;
455
+ for (let i = 0; i < content.length; i++) {
456
+ if (code[i] === '(' || code[i] === '[') {
457
+ brackets++;
458
+ }
459
+ else if (code[i] === ')' || code[i] === ']') {
460
+ brackets--;
461
+ }
462
+ if (!code.startsWith(opening, i) || this.isOwnMember(code, i, owner, scopes, indented && !brackets)) {
463
+ continue;
464
+ }
465
+ const bodyStart = i + opening.length;
466
+ const bodyEnd = this.findClosingBracket(content, bodyStart);
467
+ if (bodyEnd === -1) {
468
+ // unbalanced from here on, leave this call rather than the rest of the file
469
+ continue;
470
+ }
471
+ calls.push({ bodyStart, bodyEnd });
472
+ // a same-owner call nested in this one is already covered by it
473
+ i = bodyEnd;
474
+ }
475
+ return calls;
476
+ }
477
+ /**
478
+ * Returns the index of the bracket closing the one `start` is inside of, or -1 when the
479
+ * content is unbalanced. Brackets in strings, comments and `url()` tokens are ignored.
480
+ */
481
+ findClosingBracket(content, start) {
482
+ let level = 0;
483
+ for (let i = start; i < content.length; i++) {
484
+ const nonCodeEnd = this.skipNonCode(content, i);
485
+ if (nonCodeEnd !== -1) {
486
+ i = nonCodeEnd;
487
+ continue;
488
+ }
489
+ const char = content[i];
490
+ if (char === '(') {
491
+ level++;
492
+ }
493
+ else if (char === ')') {
494
+ if (!level) {
495
+ return i;
496
+ }
497
+ level--;
498
+ }
499
+ }
500
+ return -1;
501
+ }
502
+ /** Collects declarations within their brace or indentation scopes in code with non-code masked. */
503
+ declarationScopes(content, code, indented) {
504
+ const scopes = [];
505
+ const stack = [];
506
+ let brackets = 0;
507
+ let pendingDeclaration;
508
+ const enterScope = (start, indentation = 0) => {
509
+ const scope = {
510
+ start, end: code.length, mixins: new Map(), functions: new Map(), declarations: []
511
+ };
512
+ scopes.push(scope);
513
+ // Braces in an interpolated default argument do not start the declaration's body.
514
+ const declaration = brackets ? undefined : pendingDeclaration;
515
+ stack.push({ scope, indentation, declaration });
516
+ if (declaration) {
517
+ pendingDeclaration = undefined;
518
+ }
519
+ };
520
+ const leaveScope = (end) => {
521
+ const { scope, declaration } = stack.pop();
522
+ scope.end = end;
523
+ if (declaration) {
524
+ declaration.end = end;
525
+ }
526
+ };
527
+ enterScope(0);
528
+ const declaration = /(?:@(mixin|function)\s+|=\s*)([\w-]+)/y;
529
+ for (let i = 0; i < code.length; i++) {
530
+ // Indentation inside a multiline argument/list is not a new Sass scope.
531
+ if (indented && !brackets && (i === 0 || code[i - 1] === '\n')) {
532
+ const lineEnd = code.indexOf('\n', i);
533
+ const line = code.substring(i, lineEnd === -1 ? code.length : lineEnd);
534
+ if (line.trim()) {
535
+ // Masked comments are whitespace too, but are not part of the source indentation.
536
+ const indentation = /^[ \t]*/.exec(content.substring(i))[0].length;
537
+ // An indented mixin may have defaults but no body.
538
+ if (pendingDeclaration && indentation <= stack[stack.length - 1].indentation) {
539
+ pendingDeclaration.end = i;
540
+ pendingDeclaration = undefined;
541
+ }
542
+ while (stack.length > 1 && indentation < stack[stack.length - 1].indentation) {
543
+ leaveScope(i);
544
+ }
545
+ if (indentation > stack[stack.length - 1].indentation) {
546
+ enterScope(i, indentation);
547
+ }
548
+ }
549
+ }
550
+ const char = code[i];
551
+ if (!indented && char === '{') {
552
+ enterScope(i);
553
+ }
554
+ else if (!indented && char === '}' && stack.length > 1) {
555
+ leaveScope(i);
556
+ }
557
+ else if (char === '(' || char === '[') {
558
+ brackets++;
559
+ }
560
+ else if (char === ')' || char === ']') {
561
+ brackets--;
562
+ }
563
+ else if (char === '@' || (indented && !brackets && char === '='
564
+ && this.keywordBefore(code, i + 1, true) === '@mixin')) {
565
+ declaration.lastIndex = i;
566
+ const match = declaration.exec(code);
567
+ if (match) {
568
+ const scope = stack[stack.length - 1].scope;
569
+ // Sass treats underscores and hyphens as the same identifier character.
570
+ const members = scope[match[1] === 'function' ? 'functions' : 'mixins'];
571
+ const name = match[2].replace(/_/g, '-');
572
+ if (!members.has(name)) {
573
+ members.set(name, i);
574
+ }
575
+ pendingDeclaration = { start: i, end: code.length };
576
+ scope.declarations.push(pendingDeclaration);
577
+ i = declaration.lastIndex - 1;
578
+ }
579
+ }
580
+ }
581
+ return scopes;
582
+ }
583
+ /**
584
+ * Tells whether the `owner` at `index` is part of a member the stylesheet declares itself. A
585
+ * user's own `app-grid-summary-theme` ends with a theme function name, so without this its
586
+ * declaration would lose a parameter its body reads.
587
+ *
588
+ * Only this stylesheet is read. Telling which member a call loaded from another one refers to
589
+ * needs the module rules of `@use` and `@forward` - re-exports, `hide` and `as prefix-*` - so a
590
+ * member of the app that another stylesheet declares is still taken for the Ignite UI one.
591
+ */
592
+ isOwnMember(content, index, owner, scopes, indented) {
593
+ let start = index;
594
+ while (start > 0 && /[\w-]/.test(content[start - 1])) {
595
+ start--;
596
+ }
597
+ // a namespace holds the members of another stylesheet, never the ones declared here
598
+ if (content[start - 1] === '.') {
599
+ return false;
600
+ }
601
+ const keyword = this.keywordBefore(content, start, indented);
602
+ if (keyword === '@mixin' || keyword === '@function') {
603
+ // a declaration, whose body reads the parameters it lists
604
+ return true;
605
+ }
606
+ const name = content.substring(start, index + owner.length).replace(/_/g, '-');
607
+ // Bodies and defaults can run after a later declaration becomes available. Keep their
608
+ // existing protection; declaration order is conclusive only for immediate calls.
609
+ const deferred = scopes.some(scope => scope.declarations.some(member => member.start <= index && index < member.end));
610
+ // a mixin and a function of one name are separate members, so only the called one shadows
611
+ return scopes.some(scope => {
612
+ const declaredAt = (keyword === '@include' ? scope.mixins : scope.functions).get(name);
613
+ return scope.start <= index && index < scope.end && declaredAt !== undefined
614
+ && (deferred || declaredAt <= index);
615
+ });
616
+ }
617
+ /** The at-rule before a member name, or indented shorthand when outside an expression. */
618
+ keywordBefore(content, start, indented) {
619
+ let end = start;
620
+ while (end > 0 && /\s/.test(content[end - 1])) {
621
+ end--;
622
+ }
623
+ // Shorthand starts a statement. Unlike '=', '+' must be adjacent to the mixin name.
624
+ const shorthand = content[end - 1];
625
+ if (indented && (shorthand === '=' || (shorthand === '+' && end === start))
626
+ && !content.substring(content.lastIndexOf('\n', end - 1) + 1, end - 1).trim()) {
627
+ return shorthand === '=' ? '@mixin' : '@include';
628
+ }
629
+ let keywordStart = end;
630
+ while (keywordStart > 0 && /[\w@-]/.test(content[keywordStart - 1])) {
631
+ keywordStart--;
632
+ }
633
+ return content.substring(keywordStart, end);
634
+ }
635
+ /** Masks non-code with whitespace, preserving offsets, line endings and the original stylesheet. */
636
+ sassCode(content) {
637
+ const parts = [];
638
+ let start = 0;
639
+ for (let i = 0; i < content.length; i++) {
640
+ const nonCodeEnd = this.skipNonCode(content, i);
641
+ if (nonCodeEnd !== -1) {
642
+ parts.push(content.substring(start, i), content.substring(i, nonCodeEnd + 1).replace(/[^\r\n]/g, ' '));
643
+ start = nonCodeEnd + 1;
644
+ i = nonCodeEnd;
645
+ }
646
+ }
647
+ parts.push(content.substring(start));
648
+ return parts.join('');
649
+ }
650
+ /** Index where an argument's code starts, after its leading whitespace and comments. */
651
+ leadingCommentsEnd(param) {
652
+ let i = 0;
653
+ while (i < param.length) {
654
+ if (/\s/.test(param[i])) {
655
+ i++;
656
+ }
657
+ else if (param[i] === '/' && (param[i + 1] === '/' || param[i + 1] === '*')) {
658
+ i = this.skipNonCode(param, i) + 1;
659
+ }
660
+ else {
661
+ break;
662
+ }
663
+ }
664
+ return i;
665
+ }
666
+ /** The line break a carried `//` comment needs so that it does not swallow what follows it. */
667
+ commentBreak(carried, next, eol) {
668
+ return /\/\/[^\n]*$/.test(carried) && !/^[ \t]*\r?\n/.test(next) ? eol : '';
669
+ }
670
+ /**
671
+ * When a string, a comment or a `url()` token starts at `index`, returns the index of its last
672
+ * character so that a scan can carry on past it. Returns -1 when `index` is on code.
673
+ */
674
+ skipNonCode(content, index) {
675
+ const char = content[index];
676
+ const next = content[index + 1];
677
+ if (char === '\'' || char === '"') {
678
+ return this.skipString(content, index);
679
+ }
680
+ if (char === '/' && next === '*') {
681
+ const end = content.indexOf('*/', index + 2);
682
+ return end === -1 ? content.length : end + 1;
683
+ }
684
+ if (char === '/' && next === '/') {
685
+ const end = content.indexOf('\n', index + 2);
686
+ return end === -1 ? content.length : end;
687
+ }
688
+ if (this.isUrlToken(content, index)) {
689
+ return this.skipUrl(content, index + URL_TOKEN.length);
690
+ }
691
+ return -1;
692
+ }
693
+ /**
694
+ * Sass parses an unquoted `url()` as a single token, so a `//` inside one is part of the
695
+ * address - `url(//cdn.example.com/bg.png)` - and not the start of a comment.
696
+ */
697
+ isUrlToken(content, index) {
698
+ if (content.substr(index, URL_TOKEN.length).toLowerCase() !== URL_TOKEN) {
699
+ return false;
700
+ }
701
+ const previous = content[index - 1];
702
+ return previous === undefined || !/[\w-]/.test(previous);
703
+ }
704
+ /** Returns the index of the bracket closing a `url(` whose contents start at `start`. */
705
+ skipUrl(content, start) {
706
+ for (let i = start; i < content.length; i++) {
707
+ const char = content[i];
708
+ if (char === '\'' || char === '"') {
709
+ i = this.skipString(content, i);
710
+ }
711
+ else if (char === ')') {
712
+ return i;
713
+ }
714
+ }
715
+ return content.length;
716
+ }
717
+ /** Returns the index of the quote closing the string opened at `start`. */
718
+ skipString(content, start) {
719
+ const quote = content[start];
720
+ for (let i = start + 1; i < content.length; i++) {
721
+ if (content[i] === '\\') {
722
+ i++;
723
+ }
724
+ else if (content[i] === quote) {
725
+ return i;
726
+ }
727
+ }
728
+ return content.length;
729
+ }
410
730
  isNamedArgument(fileContent, i, occurrences, change) {
411
731
  const openingBrackets = [];
412
732
  const closingBrackets = [];
@@ -777,12 +1097,20 @@ class UpdateChanges {
777
1097
  * prop: inner-func(),
778
1098
  * prop2: inner2(inner-param: 3, inner-param: inner-func(..))
779
1099
  * ```
1100
+ *
1101
+ * Strings, comments and `url()` tokens are scanned over the same way the rest of the scanner
1102
+ * does it, so a `','` inside one of them is not taken for an argument separator.
780
1103
  */
781
1104
  splitFunctionProps(body) {
782
1105
  const parts = [];
783
1106
  let lastIndex = 0;
784
1107
  let level = 0;
785
1108
  for (let i = 0; i < body.length; i++) {
1109
+ const nonCodeEnd = this.skipNonCode(body, i);
1110
+ if (nonCodeEnd !== -1) {
1111
+ i = nonCodeEnd;
1112
+ continue;
1113
+ }
786
1114
  const char = body[i];
787
1115
  switch (char) {
788
1116
  case '(':