Skip to content

Commit

Permalink
feat: clean string module lex string template (#7667)
Browse files Browse the repository at this point in the history
  • Loading branch information
poyoho committed Apr 11, 2022
1 parent 9ed1672 commit dfce283
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 1 deletion.
52 changes: 52 additions & 0 deletions packages/vite/src/node/__tests__/cleanString.spec.ts
Expand Up @@ -106,3 +106,55 @@ test('find empty string flag in raw index', () => {
const bStart = clean.indexOf('\0\0\0\0\0', bIndex)
expect(str.slice(bStart, bStart + 5)).toMatch('bbbbb')
})

test('template string nested', () => {
let str = '`aaaa`'
let res = '`\0\0\0\0`'
let clean = emptyString(str)
expect(clean).toMatch(res)

str = '`aaaa` `aaaa`'
res = '`\0\0\0\0` `\0\0\0\0`'
clean = emptyString(str)
expect(clean).toMatch(res)

str = '`aa${a}aa`'
res = '`\0\0${a}\0\0`'
clean = emptyString(str)
expect(clean).toMatch(res)

str = '`aa${a + `a` + a}aa`'
res = '`\0\0${a + `\0` + a}\0\0`'
clean = emptyString(str)
expect(clean).toMatch(res)

str = '`aa${a + `a` + a}aa` `aa${a + `a` + a}aa`'
res = '`\0\0${a + `\0` + a}\0\0` `\0\0${a + `\0` + a}\0\0`'
clean = emptyString(str)
expect(clean).toMatch(res)

str = '`aa${a + `aaaa${c + (a = {b: 1}) + d}` + a}aa`'
res = '`\0\0${a + `\0\0\0\0${c + (a = {b: 1}) + d}` + a}\0\0`'
clean = emptyString(str)
expect(clean).toMatch(res)

str =
'`aa${a + `aaaa${c + (a = {b: 1}) + d}` + a}aa` `aa${a + `aaaa${c + (a = {b: 1}) + d}` + a}aa`'
res =
'`\0\0${a + `\0\0\0\0${c + (a = {b: 1}) + d}` + a}\0\0` `\0\0${a + `\0\0\0\0${c + (a = {b: 1}) + d}` + a}\0\0`'
clean = emptyString(str)
expect(clean).toMatch(res)

str = '`aaaa'
res = ''
try {
clean = emptyString(str)
} catch {}
expect(clean).toMatch(res)

str =
"<img src=\"${new URL('../assets/images/loading/loading.gif', import.meta.url).href}\" alt=''>"
res = `<img src="\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0" alt=''>`
clean = emptyString(str)
expect(clean).toMatch(res)
})
113 changes: 112 additions & 1 deletion packages/vite/src/node/cleanString.ts
@@ -1,3 +1,4 @@
import type { RollupError } from 'rollup'
// bank on the non-overlapping nature of regex matches and combine all filters into one giant regex
// /`([^`\$\{\}]|\$\{(`|\g<1>)*\})*`/g can match nested string template
// but js not support match expression(\g<0>). so clean string template(`...`) in other ways.
Expand All @@ -8,7 +9,117 @@ const stringBlankReplacer = (s: string) =>
`${s[0]}${'\0'.repeat(s.length - 2)}${s[0]}`

export function emptyString(raw: string): string {
return raw.replace(cleanerRE, (s: string) =>
let res = raw.replace(cleanerRE, (s: string) =>
s[0] === '/' ? blankReplacer(s) : stringBlankReplacer(s)
)

let lastEnd = 0
let start = 0
while ((start = res.indexOf('`', lastEnd)) >= 0) {
let clean
;[clean, lastEnd] = lexStringTemplateExpression(res, start)
res = replaceAt(res, start, lastEnd, clean)
}

return res
}

const enum LexerState {
inTemplateString,
inInterpolationExpression,
inObjectExpression
}

function replaceAt(
string: string,
start: number,
end: number,
replacement: string
): string {
return string.slice(0, start) + replacement + string.slice(end)
}

/**
* lex string template and clean it.
*/
function lexStringTemplateExpression(
code: string,
start: number
): [string, number] {
let state = LexerState.inTemplateString as LexerState
let clean = '`'
const opStack: LexerState[] = [state]

function pushStack(newState: LexerState) {
state = newState
opStack.push(state)
}

function popStack() {
opStack.pop()
state = opStack[opStack.length - 1]
}

let i = start + 1
outer: for (; i < code.length; i++) {
const char = code.charAt(i)
switch (state) {
case LexerState.inTemplateString:
if (char === '$' && code.charAt(i + 1) === '{') {
pushStack(LexerState.inInterpolationExpression)
clean += '${'
i++ // jump next
} else if (char === '`') {
popStack()
clean += char
if (opStack.length === 0) {
break outer
}
} else {
clean += '\0'
}
break
case LexerState.inInterpolationExpression:
if (char === '{') {
pushStack(LexerState.inObjectExpression)
clean += char
} else if (char === '}') {
popStack()
clean += char
} else if (char === '`') {
pushStack(LexerState.inTemplateString)
clean += char
} else {
clean += char
}
break
case LexerState.inObjectExpression:
if (char === '}') {
popStack()
clean += char
} else if (char === '`') {
pushStack(LexerState.inTemplateString)
clean += char
} else {
clean += char
}
break
default:
throw new Error('unknown string template lexer state')
}
}

if (opStack.length !== 0) {
error(start)
}

return [clean, i + 1]
}

function error(pos: number) {
const err = new Error(
`can not match string template expression.`
) as RollupError
err.pos = pos
throw err
}

0 comments on commit dfce283

Please sign in to comment.