refactor(migrations): Fixes offset calculations for nesting (#52332)

This updates offset to handle pre and post offset properly for nested situations, rather than relying on solely nestCount. This should properly apply offset calculations at the right time to handle any nested situation.

PR Close #52332
This commit is contained in:
Jessica Janiuk
2023-10-23 09:52:00 -04:00
committed by Dylan Hunn
parent 0c1cf7ac6b
commit 6a3a7b0d13
3 changed files with 1044 additions and 254 deletions
@@ -9,18 +9,25 @@
import {Attribute, Element, RecursiveVisitor} from '@angular/compiler';
export const ngif = '*ngIf';
export const boundngif = '[ngIf]';
export const nakedngif = 'ngIf';
export const ngfor = '*ngFor';
export const ngswitch = '[ngSwitch]';
const attributesToMigrate = [
ngif,
nakedngif,
boundngif,
ngfor,
ngswitch,
];
const casesToMigrate = [
'[ngSwitchCase]',
'*ngSwitchCase',
'ngSwitchCase',
'*ngSwitchDefault',
'ngSwitchDefault',
];
/**
@@ -29,6 +36,16 @@ const casesToMigrate = [
*/
type Range = [start: number, end?: number];
export type Offsets = {
pre: number,
post: number,
};
export type Result = {
tmpl: string,
offsets: Offsets,
};
/**
* Represents an error that happened during migration
*/
@@ -44,6 +61,7 @@ export class ElementToMigrate {
el: Element;
attr: Attribute;
nestCount = 0;
lineBreaks = false;
constructor(el: Element, attr: Attribute) {
this.el = el;
@@ -65,32 +83,16 @@ export class ElementToMigrate {
}
start(offset: number): number {
return this.el.sourceSpan?.start.offset - this.nestCount - offset;
return this.el.sourceSpan?.start.offset - offset;
}
end(offset: number): number {
return this.el.sourceSpan?.end.offset - this.nestCount - offset;
return this.el.sourceSpan?.end.offset - offset;
}
length(): number {
return this.el.sourceSpan?.end.offset - this.el.sourceSpan?.start.offset;
}
openLength(): number {
return this.el.children[0]?.sourceSpan.start.offset - this.el.sourceSpan?.start.offset;
}
closeLength(): number {
return this.el.sourceSpan?.end.offset - this.el.children[0]?.sourceSpan.end.offset;
}
preOffset(newOffset: number): number {
return newOffset - this.openLength() + 1;
}
postOffset(newOffset: number): number {
return newOffset - this.closeLength();
}
}
export class Template {
@@ -98,6 +100,7 @@ export class Template {
count: number = 0;
contents: string = '';
children: string = '';
used: boolean = false;
constructor(el: Element) {
this.el = el;
@@ -10,7 +10,7 @@ import {HtmlParser, Node, ParseTreeResult, visitAll} from '@angular/compiler';
import {dirname, join} from 'path';
import ts from 'typescript';
import {AnalyzedFile, CaseCollector, ElementCollector, ElementToMigrate, MigrateError, ngfor, ngif, ngswitch, Template} from './types';
import {AnalyzedFile, boundngif, CaseCollector, ElementCollector, ElementToMigrate, MigrateError, nakedngif, ngfor, ngif, ngswitch, Result, Template} from './types';
/**
* Analyzes a source file to find file that need to be migrated and the text ranges within them.
@@ -86,6 +86,8 @@ function getNestedCount(etm: ElementToMigrate, aggregator: number[]) {
}
}
const lb = '\n';
/**
* Replaces structural directive control flow instances with block control flow equivalents.
* Returns null if the migration failed (e.g. there was a syntax error).
@@ -104,6 +106,7 @@ export function migrateTemplate(template: string): {migrated: string|null, error
tokenizeExpansionForms: true,
// Explicitly disable blocks so that their characters are treated as plain text.
tokenizeBlocks: false,
preserveLineEndings: true,
});
// Don't migrate invalid templates.
@@ -119,6 +122,9 @@ export function migrateTemplate(template: string): {migrated: string|null, error
}
let result = template;
const lineBreaks = template.match(/\r|\n/g);
const hasLineBreaks = lineBreaks !== null;
const visitor = new ElementCollector();
visitAll(visitor, parsed.rootNodes);
@@ -137,6 +143,9 @@ export function migrateTemplate(template: string): {migrated: string|null, error
for (let i = 1; i < visitor.elements.length; i++) {
let currEl = visitor.elements[i];
currEl.nestCount = getNestedCount(currEl, nestedQueue);
if (currEl.el.sourceSpan.end.offset !== nestedQueue[nestedQueue.length - 1]) {
nestedQueue.push(currEl.el.sourceSpan.end.offset);
}
}
// this tracks the character shift from different lengths of blocks from
@@ -144,38 +153,48 @@ export function migrateTemplate(template: string): {migrated: string|null, error
// migration. Each block calculates length differences and passes that offset
// to the next migrating block to adjust character offsets properly.
let offset = 0;
let nestLevel = -1;
let postOffsets: number[] = [];
let migrateResult: Result = {tmpl: result, offsets: {pre: 0, post: 0}};
for (const el of visitor.elements) {
// these are all migratable nodes
// applies the post offsets after closing
if (el.nestCount <= nestLevel) {
const count = nestLevel - el.nestCount;
// reduced nesting, add postoffset
for (let i = 0; i <= count; i++) {
offset += postOffsets.pop() ?? 0;
}
}
if (el.attr.name === ngif) {
// these are all migratable nodes
if (el.attr.name === ngif || el.attr.name === nakedngif || el.attr.name === boundngif) {
try {
let ifResult = migrateNgIf(el, visitor.templates, result, offset);
result = ifResult.tmpl;
offset = ifResult.offset;
migrateResult = migrateNgIf(el, visitor.templates, result, offset, hasLineBreaks);
} catch (error: unknown) {
errors.push({type: ngfor, error});
errors.push({type: ngif, error});
}
} else if (el.attr.name === ngfor) {
try {
let forResult = migrateNgFor(el, result, offset);
result = forResult.tmpl;
offset = forResult.offset;
migrateResult = migrateNgFor(el, result, offset, hasLineBreaks);
} catch (error: unknown) {
errors.push({type: ngfor, error});
}
} else if (el.attr.name === ngswitch) {
try {
let switchResult = migrateNgSwitch(el, result, offset);
result = switchResult.tmpl;
offset = switchResult.offset;
migrateResult = migrateNgSwitch(el, result, offset, hasLineBreaks);
} catch (error: unknown) {
errors.push({type: ngfor, error});
errors.push({type: ngswitch, error});
}
}
result = migrateResult.tmpl;
offset += migrateResult.offsets.pre;
postOffsets.push(migrateResult.offsets.post);
const nm = el.el.name;
nestLevel = el.nestCount;
}
for (const [_, t] of visitor.templates) {
if (t.count < 2) {
if (t.count < 2 && t.used) {
result = result.replace(t.contents, '');
}
}
@@ -183,14 +202,122 @@ export function migrateTemplate(template: string): {migrated: string|null, error
return {migrated: result, errors};
}
function migrateNgIf(
etm: ElementToMigrate, ngTemplates: Map<string, Template>, tmpl: string, offset: number,
hasLineBreaks: boolean): Result {
const matchThen = etm.attr.value.match(/;\s+then/gm);
const matchElse = etm.attr.value.match(/;\s+else/gm);
if (matchThen && matchThen.length > 0) {
return buildIfThenElseBlock(
etm, ngTemplates, tmpl, matchThen[0], matchElse![0], offset, hasLineBreaks);
} else if (matchElse && matchElse.length > 0) {
// just else
return buildIfElseBlock(etm, ngTemplates, tmpl, matchElse[0], offset, hasLineBreaks);
}
return buildIfBlock(etm, tmpl, offset, hasLineBreaks);
}
function buildIfBlock(
etm: ElementToMigrate, tmpl: string, offset: number, hasLineBreaks: boolean): Result {
// includes the mandatory semicolon before as
const lbString = hasLineBreaks ? lb : '';
const condition = etm.attr.value.replace(' as ', '; as ');
const originals = getOriginals(etm, tmpl, offset);
const {start, middle, end} = getMainBlock(etm, tmpl, offset);
const startBlock = `@if (${condition}) {${lbString}${start}`;
const endBlock = `${end}${lbString}}`;
const ifBlock = startBlock + middle + endBlock;
const updatedTmpl = tmpl.slice(0, etm.start(offset)) + ifBlock + tmpl.slice(etm.end(offset));
// this should be the difference between the starting element up to the start of the closing
// element and the mainblock sans }
const pre = originals.start.length - startBlock.length;
const post = originals.end.length - endBlock.length;
return {tmpl: updatedTmpl, offsets: {pre, post}};
}
function buildIfElseBlock(
etm: ElementToMigrate, ngTemplates: Map<string, Template>, tmpl: string, elseString: string,
offset: number, hasLineBreaks: boolean): Result {
// includes the mandatory semicolon before as
const lbString = hasLineBreaks ? lb : '';
const condition = etm.getCondition(elseString).replace(' as ', '; as ');
const originals = getOriginals(etm, tmpl, offset);
const elseTmpl = ngTemplates.get(`#${etm.getTemplateName(elseString)}`)!;
const {start, middle, end} = getMainBlock(etm, tmpl, offset);
const startBlock = `@if (${condition}) {${lbString}${start}`;
const elseBlock = `${end}${lbString}} @else {${lbString}`;
const postBlock = elseBlock + elseTmpl.children + `${lbString}}`;
const ifElseBlock = startBlock + middle + postBlock;
const tmplStart = tmpl.slice(0, etm.start(offset));
const tmplEnd = tmpl.slice(etm.end(offset));
const updatedTmpl = tmplStart + ifElseBlock + tmplEnd;
// decrease usage count of elseTmpl
elseTmpl.count--;
elseTmpl.used = true;
const pre = originals.start.length - startBlock.length;
const post = originals.end.length - postBlock.length;
return {tmpl: updatedTmpl, offsets: {pre, post}};
}
function buildIfThenElseBlock(
etm: ElementToMigrate, ngTemplates: Map<string, Template>, tmpl: string, thenString: string,
elseString: string, offset: number, hasLineBreaks: boolean): Result {
const condition = etm.getCondition(thenString).replace(' as ', '; as ');
const lbString = hasLineBreaks ? lb : '';
const originals = getOriginals(etm, tmpl, offset);
const startBlock = `@if (${condition}) {${lbString}`;
const elseBlock = `${lbString}} @else {${lbString}`;
const thenTmpl = ngTemplates.get(`#${etm.getTemplateName(thenString, elseString)}`)!;
const elseTmpl = ngTemplates.get(`#${etm.getTemplateName(elseString)}`)!;
const postBlock = thenTmpl.children + elseBlock + elseTmpl.children + `${lbString}}`;
const ifThenElseBlock = startBlock + postBlock;
const tmplStart = tmpl.slice(0, etm.start(offset));
const tmplEnd = tmpl.slice(etm.end(offset));
const updatedTmpl = tmplStart + ifThenElseBlock + tmplEnd;
// decrease usage count of thenTmpl and elseTmpl
thenTmpl.count--;
thenTmpl.used = true;
elseTmpl.count--;
elseTmpl.used = true;
const pre = originals.start.length - startBlock.length;
const post = originals.end.length - postBlock.length;
return {tmpl: updatedTmpl, offsets: {pre, post}};
}
function migrateNgFor(
etm: ElementToMigrate, tmpl: string, offset: number): {tmpl: string, offset: number} {
etm: ElementToMigrate, tmpl: string, offset: number, hasLineBreaks: boolean): Result {
const aliasWithEqualRegexp = /=\s+(count|index|first|last|even|odd)/gm;
const aliasWithAsRegexp = /(count|index|first|last|even|odd)\s+as/gm;
const aliases = [];
const lbString = hasLineBreaks ? lb : '';
const lbSpaces = hasLineBreaks ? `${lb} ` : '';
const parts = etm.attr.value.split(';');
const originals = getOriginals(etm, tmpl, offset);
// first portion should always be the loop definition prefixed with `let`
const condition = parts[0].replace('let ', '');
const loopVar = condition.split(' of ')[0];
@@ -222,148 +349,97 @@ function migrateNgFor(
const aliasStr = (aliases.length > 0) ? `;${aliases.join(';')}` : '';
const startBlock = `@for (${condition}; track ${trackBy}${aliasStr}) {`;
const {start, middle, end} = getMainBlock(etm, tmpl, offset);
const startBlock = `@for (${condition}; track ${trackBy}${aliasStr}) {${lbSpaces}${start}`;
const mainBlock = getMainBlock(etm, tmpl, offset);
const forBlock = startBlock + mainBlock + '}';
const endBlock = `${end}${lbString}}`;
const forBlock = startBlock + middle + endBlock;
const updatedTmpl = tmpl.slice(0, etm.start(offset)) + forBlock + tmpl.slice(etm.end(offset));
offset = offset + etm.length() - forBlock.length;
const pre = originals.start.length - startBlock.length;
const post = originals.end.length - endBlock.length;
return {tmpl: updatedTmpl, offset};
return {tmpl: updatedTmpl, offsets: {pre, post}};
}
function migrateNgIf(
etm: ElementToMigrate, ngTemplates: Map<string, Template>, tmpl: string,
offset: number): {tmpl: string, offset: number} {
const matchThen = etm.attr.value.match(/;\s+then/gm);
const matchElse = etm.attr.value.match(/;\s+else/gm);
function getOriginals(
etm: ElementToMigrate, tmpl: string, offset: number): {start: string, end: string} {
// original opening block
if (etm.el.children.length > 0) {
const start = tmpl.slice(
etm.el.sourceSpan.start.offset - offset,
etm.el.children[0].sourceSpan.start.offset - offset);
// original closing block
const end = tmpl.slice(
etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset,
etm.el.sourceSpan.end.offset - offset);
return {start, end};
}
// self closing or no children
const start =
tmpl.slice(etm.el.sourceSpan.start.offset - offset, etm.el.sourceSpan.end.offset - offset);
// original closing block
return {start, end: ''};
}
if (matchThen && matchThen.length > 0) {
return buildIfThenElseBlock(etm, ngTemplates, tmpl, matchThen[0], matchElse![0], offset);
} else if (matchElse && matchElse.length > 0) {
// just else
return buildIfElseBlock(etm, ngTemplates, tmpl, matchElse[0], offset);
function getMainBlock(etm: ElementToMigrate, tmpl: string, offset: number):
{start: string, middle: string, end: string} {
if (etm.el.name === 'ng-container' && etm.el.attrs.length === 1) {
// this is the case where we're migrating and there's no need to keep the ng-container
const childStart = etm.el.children[0].sourceSpan.start.offset - offset;
const childEnd = etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset;
const middle = tmpl.slice(childStart, childEnd);
return {start: '', middle, end: ''};
}
return buildIfBlock(etm, tmpl, offset);
}
const attrStart = etm.attr.keySpan!.start.offset - 1 - offset;
const valEnd = etm.attr.valueSpan!.end.offset + 1 - offset;
let childStart = valEnd;
let childEnd = valEnd;
function buildIfBlock(
etm: ElementToMigrate, tmpl: string, offset: number): {tmpl: string, offset: number} {
// includes the mandatory semicolon before as
const condition = etm.attr.value.replace(' as ', '; as ');
const startBlock = `@if (${condition}) {`;
const ifBlock = startBlock + getMainBlock(etm, tmpl, offset) + `}`;
const updatedTmpl = tmpl.slice(0, etm.start(offset)) + ifBlock + tmpl.slice(etm.end(offset));
offset = offset + etm.length() - ifBlock.length;
return {tmpl: updatedTmpl, offset};
}
function buildIfElseBlock(
etm: ElementToMigrate, ngTemplates: Map<string, Template>, tmpl: string, elseString: string,
offset: number): {tmpl: string, offset: number} {
// includes the mandatory semicolon before as
const condition = etm.getCondition(elseString).replace(' as ', '; as ');
const elseTmpl = ngTemplates.get(`#${etm.getTemplateName(elseString)}`)!;
const startBlock = `@if (${condition}) {`;
const mainBlock = getMainBlock(etm, tmpl, offset);
const elseBlock = `} @else {`;
const postBlock = elseBlock + elseTmpl.children + '}';
const ifElseBlock = startBlock + mainBlock + postBlock;
let tmplStart = tmpl.slice(0, etm.start(offset));
let tmplEnd = tmpl.slice(etm.end(offset));
const updatedTmpl = tmplStart + ifElseBlock + tmplEnd;
offset = offset + etm.preOffset(startBlock.length) +
etm.postOffset(mainBlock.length + postBlock.length);
// decrease usage count of elseTmpl
elseTmpl.count--;
return {tmpl: updatedTmpl, offset};
}
function buildIfThenElseBlock(
etm: ElementToMigrate, ngTemplates: Map<string, Template>, tmpl: string, thenString: string,
elseString: string, offset: number): {tmpl: string, offset: number} {
const condition = etm.getCondition(thenString).replace(' as ', '; as ');
const startBlock = `@if (${condition}) {`;
const elseBlock = `} @else {`;
const thenTmpl = ngTemplates.get(`#${etm.getTemplateName(thenString, elseString)}`)!;
const elseTmpl = ngTemplates.get(`#${etm.getTemplateName(elseString)}`)!;
const postBlock = thenTmpl.children + elseBlock + elseTmpl.children + '}';
const ifThenElseBlock = startBlock + postBlock;
let tmplStart = tmpl.slice(0, etm.start(offset));
let tmplEnd = tmpl.slice(etm.end(offset));
const updatedTmpl = tmplStart + ifThenElseBlock + tmplEnd;
offset = offset + etm.preOffset(startBlock.length) + etm.postOffset(postBlock.length);
// decrease usage count of thenTmpl and elseTmpl
thenTmpl.count--;
elseTmpl.count--;
return {tmpl: updatedTmpl, offset};
}
function getMainBlock(etm: ElementToMigrate, tmpl: string, offset: number) {
if (etm.el.name === 'ng-container' && etm.el.attrs.length === 1 && etm.attr.name === ngfor) {
// this is the case where we're migrating an ngFor and there's no need to keep the ng-container
const childStart = etm.el.children[0].sourceSpan.start.offset - etm.nestCount - offset;
const childEnd =
etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - etm.nestCount - offset;
return tmpl.slice(childStart, childEnd);
if (etm.el.children.length > 0) {
childStart = etm.el.children[0].sourceSpan.start.offset - offset;
childEnd = etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset;
}
const attrStart = etm.attr.keySpan!.start.offset - 1 - etm.nestCount - offset;
const valEnd = etm.attr.valueSpan!.end.offset + 1 - etm.nestCount - offset;
const start = tmpl.slice(etm.start(offset), attrStart);
const end = tmpl.slice(valEnd, etm.end(offset));
return start + end;
let start = tmpl.slice(etm.start(offset), attrStart);
start += tmpl.slice(valEnd, childStart);
const middle = tmpl.slice(childStart, childEnd);
const end = tmpl.slice(childEnd, etm.end(offset));
return {start, middle, end};
}
function migrateNgSwitch(
etm: ElementToMigrate, tmpl: string, offset: number): {tmpl: string, offset: number} {
etm: ElementToMigrate, tmpl: string, offset: number, hasLineBreaks: boolean): Result {
const condition = etm.attr.value;
const startBlock = `@switch (${condition}) { `;
const startBlock = `@switch (${condition}) {`;
const lbString = hasLineBreaks ? lb : '';
const {openTag, closeTag, children} = getSwitchBlockElements(etm, tmpl, offset);
const cases = getSwitchCases(children, tmpl, etm.nestCount, offset);
const switchBlock = openTag + startBlock + cases.join(' ') + `}` + closeTag;
const cases = getSwitchCases(children, tmpl, offset, hasLineBreaks);
const switchBlock = openTag + startBlock + cases.join('') + `${lbString}}` + closeTag;
const updatedTmpl = tmpl.slice(0, etm.start(offset)) + switchBlock + tmpl.slice(etm.end(offset));
const pre = etm.length() - switchBlock.length;
const difference = etm.length() - switchBlock.length;
offset = offset + difference;
return {tmpl: updatedTmpl, offset};
return {tmpl: updatedTmpl, offsets: {pre, post: 0}};
}
function getSwitchBlockElements(etm: ElementToMigrate, tmpl: string, offset: number) {
const attrStart = etm.attr.keySpan!.start.offset - 1 - etm.nestCount - offset;
const valEnd = etm.attr.valueSpan!.end.offset + 1 - etm.nestCount - offset;
const childStart = etm.el.children[0].sourceSpan.start.offset - etm.nestCount - offset;
const childEnd =
etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - etm.nestCount - offset;
let openTag = tmpl.slice(etm.start(offset), attrStart) + tmpl.slice(valEnd, childStart);
if (tmpl.slice(childStart, childStart + 1) === '\n') {
openTag += '\n';
const attrStart = etm.attr.keySpan!.start.offset - 1 - offset;
const valEnd = etm.attr.valueSpan!.end.offset + 1 - offset;
const childStart = etm.el.children[0].sourceSpan.start.offset - offset;
const childEnd = etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset;
let openTag = (etm.el.name === 'ng-container') ?
'' :
tmpl.slice(etm.start(offset), attrStart) + tmpl.slice(valEnd, childStart);
if (tmpl.slice(childStart, childStart + 1) === lb) {
openTag += lb;
}
let closeTag = tmpl.slice(childEnd, etm.end(offset));
if (tmpl.slice(childEnd - 1, childEnd) === '\n') {
closeTag = '\n' + closeTag;
let closeTag = (etm.el.name === 'ng-container') ? '' : tmpl.slice(childEnd, etm.end(offset));
if (tmpl.slice(childEnd - 1, childEnd) === lb) {
closeTag = lb + closeTag;
}
return {
openTag,
@@ -372,25 +448,38 @@ function getSwitchBlockElements(etm: ElementToMigrate, tmpl: string, offset: num
};
}
function getSwitchCases(children: Node[], tmpl: string, nestCount: number, offset: number) {
function getSwitchCases(children: Node[], tmpl: string, offset: number, hasLineBreaks: boolean) {
const collector = new CaseCollector();
visitAll(collector, children);
return collector.elements.map(etm => getSwitchCaseBlock(etm, tmpl, nestCount, offset));
return collector.elements.map(etm => getSwitchCaseBlock(etm, tmpl, offset, hasLineBreaks));
}
function getSwitchCaseBlock(
etm: ElementToMigrate, tmpl: string, nestCount: number, offset: number): string {
const elStart = etm.el.sourceSpan?.start.offset - nestCount - offset;
const elEnd = etm.el.sourceSpan?.end.offset - nestCount - offset;
// beginning of the ngIf minus a leading space
const attrStart = etm.attr.keySpan!.start.offset - 1 - nestCount - offset;
etm: ElementToMigrate, tmpl: string, offset: number, hasLineBreaks: boolean): string {
let elStart = etm.el.sourceSpan?.start.offset - offset;
let elEnd = etm.el.sourceSpan?.end.offset - offset;
const lbString = hasLineBreaks ? '\n ' : ' ';
const lbSpaces = hasLineBreaks ? ' ' : '';
let shift = 0;
if ((etm.el.name === 'ng-container' || etm.el.name === 'ng-template') &&
etm.el.attrs.length === 1) {
// no need to keep the containers
elStart = etm.el.children[0].sourceSpan.start.offset - offset;
elEnd = etm.el.children[etm.el.children.length - 1].sourceSpan.end.offset - offset;
// account for the `>` that isn't needed
shift += 1;
}
const attrStart = etm.attr.keySpan!.start.offset - 1 - offset + shift;
// ngSwitchDefault case has no valueSpan and relies on the end of the key
const attrEnd = etm.attr.keySpan!.end.offset - nestCount - offset;
if (etm.attr.name === '*ngSwitchDefault') {
return `@default { ${tmpl.slice(elStart, attrStart) + tmpl.slice(attrEnd, elEnd)} }`;
if (etm.attr.name === '*ngSwitchDefault' || etm.attr.name === 'ngSwitchDefault') {
const attrEnd = etm.attr.keySpan!.end.offset - offset + shift;
return `${lbString}@default {${lbString}${lbSpaces}${
tmpl.slice(elStart, attrStart) + tmpl.slice(attrEnd, elEnd)}${lbString}}`;
}
// ngSwitchCase has a valueSpan
const valEnd = etm.attr.valueSpan!.end.offset + 1 - nestCount - offset;
return `@case (${etm.attr.value}) { ${
tmpl.slice(elStart, attrStart) + tmpl.slice(valEnd, elEnd)} }`;
let valEnd = etm.attr.valueSpan!.end.offset + 1 - offset + shift;
return `${lbString}@case (${etm.attr.value}) {${lbString}${lbSpaces}${
tmpl.slice(elStart, attrStart) + tmpl.slice(valEnd, elEnd)}${lbString}}`;
}
File diff suppressed because it is too large Load Diff