Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: clean string module lex string template #7667

Merged
merged 29 commits into from Apr 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
}