quote-morph
Version:
Automatically switches quotes to backticks when using ${} in JS/TS strings.
84 lines (73 loc) • 2.62 kB
text/typescript
export interface MorphSettings {
enableQuotesSingle: boolean;
enableQuotesDouble: boolean;
}
export function morphQuotesInLine(
lineText: string,
settings: MorphSettings
): string | null {
// If the line contains any backticks, avoid morphing entirely to prevent
// interfering with nested template segments embedded in quoted strings.
if (lineText.includes('`')) {
return null;
}
const quoteTypes: string[] = [];
if (settings.enableQuotesSingle) {
quoteTypes.push("'");
}
if (settings.enableQuotesDouble) {
quoteTypes.push('"');
}
if (quoteTypes.length === 0) {
return null;
}
let newLine = lineText;
let changed = false;
for (const quoteChar of quoteTypes) {
// match a quoted string allowing escaped chars inside, but require an UNESCAPED ${...} inside
// use negative lookbehind to ensure ${ is not preceded by a backslash
const pattern =
quoteChar +
'((?:\\\\.|[^' +
quoteChar +
'])*?(?<!\\\\)\\$\\{[^}]*\\}(?:\\\\.|[^' +
quoteChar +
'])*)' +
quoteChar;
const regex = new RegExp(pattern, 'g');
newLine = newLine.replace(regex, (fullMatch: string, content: string) => {
// leave alone if inner content itself is quoted/backticked (e.g. :src="`${test}`")
if (content.length >= 2) {
const first = content[0];
const last = content[content.length - 1];
if (['"', "'", '`'].includes(first) && first === last) {
return fullMatch;
}
}
// do not morph if content contains any backtick at all
// (avoids interfering with nested template segments like `<!-- ${...} -->`)
if (content.includes('`')) {
return fullMatch;
}
// do not morph if content contains escaped occurrences of the outer quote
// e.g., for single quotes: It\'s ${name}; for double quotes: It\"s ${name}
if (quoteChar === "'" && content.includes("\\'")) {
return fullMatch;
}
if (quoteChar === '"' && content.includes('\\"')) {
return fullMatch;
}
// escape content safely for template literal:
// - preserve literal backslash+backtick by doubling the backslash and escaping the backtick
// - escape any remaining raw backticks
// - preserve literal \${ by doubling the backslash so ${ is not treated as interpolation
const safeContent = content
.replace(/\\`/g, '\\\\`')
.replace(/`/g, '\\`')
.replace(/\\\$\{/g, '\\\\\\${');
changed = true;
return '`' + safeContent + '`';
});
}
return changed ? newLine : null;
}