Skip to content

Commit

Permalink
feat: support filter option
Browse files Browse the repository at this point in the history
  • Loading branch information
antfu committed Aug 2, 2023
1 parent f66ce4c commit 5895175
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 24 deletions.
51 changes: 37 additions & 14 deletions src/acorn.ts
@@ -1,15 +1,19 @@
import { tokenizer } from 'acorn'
import type { Parser, Token } from 'acorn'
import type { StripLiteralOptions } from './types'

/**
* Strip literal using Acorn's tokenizer.
*
* Will throw error if the input is not valid JavaScript.
*/
export function _stripLiteralAcorn(code: string) {
export function _stripLiteralAcorn(code: string, options?: StripLiteralOptions) {
const FILL = ' '
let result = ''
function fulfill(index: number) {

const filter = options?.filter ?? (() => true)

function fillupTo(index: number) {
if (index > result.length)
result += code.slice(result.length, index).replace(/[^\n]/g, FILL)
}
Expand All @@ -30,19 +34,38 @@ export function _stripLiteralAcorn(code: string) {
const { done, value: token } = iter.next()
if (done)
break

tokens.push(token)
fulfill(token.start)
if (token.type.label === 'string')
result += code[token.start] + FILL.repeat(token.end - token.start - 2) + code[token.end - 1]
else if (token.type.label === 'template')
result += FILL.repeat(token.end - token.start)
else if (token.type.label === 'regexp')
result += code.slice(token.start, token.end).replace(/\/(.*)\/(\w?)$/g, (_, $1, $2) => `/${FILL.repeat($1.length)}/${$2}`)
else
result += code.slice(token.start, token.end)
fillupTo(token.start)

if (token.type.label === 'string') {
const body = code.slice(token.start + 1, token.end - 1)
if (filter(body)) {
result += code[token.start] + FILL.repeat(token.end - token.start - 2) + code[token.end - 1]
continue
}
}

else if (token.type.label === 'template') {
const body = code.slice(token.start, token.end)
if (filter(body)) {
result += FILL.repeat(token.end - token.start)
continue
}
}

else if (token.type.label === 'regexp') {
const body = code.slice(token.start, token.end)
if (filter(body)) {
result += body.replace(/\/(.*)\/(\w?)$/g, (_, $1, $2) => `/${FILL.repeat($1.length)}/${$2}`)
continue
}
}

result += code.slice(token.start, token.end)
}

fulfill(code.length)
fillupTo(code.length)
}
catch (e) {
error = e
Expand All @@ -60,8 +83,8 @@ export function _stripLiteralAcorn(code: string) {
*
* Will throw error if the input is not valid JavaScript.
*/
export function stripLiteralAcorn(code: string) {
const result = _stripLiteralAcorn(code)
export function stripLiteralAcorn(code: string, options?: StripLiteralOptions) {
const result = _stripLiteralAcorn(code, options)
if (result.error)
throw result.error
return result.result
Expand Down
12 changes: 7 additions & 5 deletions src/index.ts
@@ -1,32 +1,34 @@
import { _stripLiteralAcorn } from './acorn'
import { stripLiteralRegex } from './regex'
import type { StripLiteralOptions } from './types'

export { stripLiteralAcorn, createIsLiteralPositionAcorn } from './acorn'
export { stripLiteralRegex } from './regex'
export * from './types'

/**
* Strip literal from code.
*
* Using Acorn's tokenizer first, and fallback to Regex if Acorn fails.
*/
export function stripLiteral(code: string) {
return stripLiteralDetailed(code).result
export function stripLiteral(code: string, options?: StripLiteralOptions) {
return stripLiteralDetailed(code, options).result
}

/**
* Strip literal from code, return more detailed information.
*
* Using Acorn's tokenizer first, and fallback to Regex if Acorn fails.
*/
export function stripLiteralDetailed(code: string): {
export function stripLiteralDetailed(code: string, options?: StripLiteralOptions): {
mode: 'acorn' | 'regex'
result: string
acorn: {
tokens: any[]
error?: any
}
} {
const acorn = _stripLiteralAcorn(code)
const acorn = _stripLiteralAcorn(code, options)
if (!acorn.error) {
return {
mode: 'acorn',
Expand All @@ -36,7 +38,7 @@ export function stripLiteralDetailed(code: string): {
}
return {
mode: 'regex',
result: stripLiteralRegex(acorn.result + code.slice(acorn.result.length)),
result: stripLiteralRegex(acorn.result + code.slice(acorn.result.length), options),
acorn,
}
}
12 changes: 9 additions & 3 deletions src/regex.ts
@@ -1,3 +1,5 @@
import type { StripLiteralOptions } from './types'

const multilineCommentsRE = /\/\*([^*\/])*?\*\//gms
const singlelineCommentsRE = /(?:^|\n|\r)\s*\/\/.*(?:\r|\n|$)/gm
const templateLiteralRE = /\$\{(\s*(?:|{.*}|(?!\$\{).|\n|\r)*?\s*)\}/g
Expand All @@ -12,10 +14,12 @@ const quotesRE = [
* This will be faster and can work on non-JavaScript input.
* But will have some caveats on distinguish strings and comments.
*/
export function stripLiteralRegex(code: string) {
export function stripLiteralRegex(code: string, options?: StripLiteralOptions) {
const filter = options?.filter ?? (() => true)

code = code
.replace(multilineCommentsRE, s => ' '.repeat(s.length))
.replace(singlelineCommentsRE, s => ' '.repeat(s.length))
.replace(multilineCommentsRE, s => filter(s) ? ' '.repeat(s.length) : s)
.replace(singlelineCommentsRE, s => filter(s) ? ' '.repeat(s.length) : s)

let expanded = code
// Recursively replace ${} to support nested constructs (e.g. ${`${x}`})
Expand All @@ -29,6 +33,8 @@ export function stripLiteralRegex(code: string) {
quotesRE.forEach((re) => {
expanded = expanded
.replace(re, (s, quote, body, index) => {
if (!filter(s.slice(1, -1)))
return s
code = code.slice(0, index + 1) + ' '.repeat(s.length - 2) + code.slice(index + s.length - 1)
return quote + ' '.repeat(s.length - 2) + quote
})
Expand Down
6 changes: 6 additions & 0 deletions src/types.ts
@@ -0,0 +1,6 @@
export interface StripLiteralOptions {
/**
* Will be called for each string literal. Return false to skip stripping.
*/
filter?: (s: string) => boolean
}
43 changes: 43 additions & 0 deletions test/filter.test.ts
@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest'
import { executeWithVerify } from './utils'

describe('filter', () => {
it('should filter correctly', () => {
const items: string[] = []
const result = executeWithVerify(`
// comment1
const a = 'aaaa'
/* comment2 */
const b = "bbbb"
const c = \`aaaa\${foo}dddd\${bar}\`
`.trim(),
true,
{
filter: (s) => {
items.push(s)
return s !== 'aaaa'
},
})

expect(result).toMatchInlineSnapshot(`
"// mode: acorn
const a = 'aaaa'
const b = \\" \\"
const c = \`aaaa\${foo} \${bar}\`"
`)

expect(items).toMatchInlineSnapshot(`
[
"aaaa",
"bbbb",
"aaaa",
"dddd",
"",
]
`)
})
})
5 changes: 3 additions & 2 deletions test/utils.ts
@@ -1,10 +1,11 @@
import { parse } from 'acorn'
import { expect } from 'vitest'
import type { StripLiteralOptions } from '../src'
import { stripLiteralDetailed } from '../src'

export function executeWithVerify(code: string, verifyAst = true) {
export function executeWithVerify(code: string, verifyAst = true, options?: StripLiteralOptions) {
code = code.trim()
const result = stripLiteralDetailed(code)
const result = stripLiteralDetailed(code, options)

// if (verifyAst && result.acorn.error)
// console.error(result.acorn.error)
Expand Down

0 comments on commit 5895175

Please sign in to comment.