File size: 4,000 Bytes
56fda74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import { compile } from 'stylis'

const haveSameLocation = (element1, element2) => {
  return element1.line === element2.line && element1.column === element2.column
}

const isAutoInsertedRule = element =>
  element.type === 'rule' &&
  element.parent &&
  haveSameLocation(element, element.parent)

const toInputTree = (elements, tree) => {
  for (let i = 0; i < elements.length; i++) {
    const element = elements[i]
    const { parent, children } = element

    if (!parent) {
      tree.push(element)
    } else if (!isAutoInsertedRule(element)) {
      parent.children.push(element)
    }

    if (Array.isArray(children)) {
      element.children = []
      toInputTree(children, tree)
    }
  }

  return tree
}

var stringifyTree = elements => {
  return elements
    .map(element => {
      switch (element.type) {
        case 'import':
        case 'decl':
          return element.value
        case 'comm':
          // When we encounter a standard multi-line CSS comment and it contains a '@'
          // character, we keep the comment. Some Stylis plugins, such as
          // the stylis-rtl via the cssjanus plugin, use this special comment syntax
          // to control behavior (such as: /* @noflip */). We can do this
          // with standard CSS comments because they will work with compression,
          // as opposed to non-standard single-line comments that will break compressed CSS.
          return element.props === '/' && element.value.includes('@')
            ? element.value
            : ''
        case 'rule':
          return `${element.value.replace(/&\f/g, '&')}{${stringifyTree(
            element.children
          )}}`
        default: {
          return `${element.value}{${stringifyTree(element.children)}}`
        }
      }
    })
    .join('')
}

const interleave = (strings /*: Array<*> */, interpolations /*: Array<*> */) =>
  interpolations.reduce(
    (array, interp, i) => array.concat([interp], strings[i + 1]),
    [strings[0]]
  )

function getDynamicMatches(str /*: string */) {
  const re = /xxx(\d+):xxx/gm
  let match
  const matches = []
  while ((match = re.exec(str)) !== null) {
    if (match !== null) {
      matches.push({
        value: match[0],
        p1: parseInt(match[1], 10),
        index: match.index
      })
    }
  }

  return matches
}

function replacePlaceholdersWithExpressions(
  str /*: string */,
  expressions /*: Array<*> */,
  t
) {
  const matches = getDynamicMatches(str)
  if (matches.length === 0) {
    if (str === '') {
      return []
    }
    return [t.stringLiteral(str)]
  }
  const strings = []
  const finalExpressions = []
  let cursor = 0

  matches.forEach(({ value, p1, index }, i) => {
    const preMatch = str.substring(cursor, index)
    cursor = cursor + preMatch.length + value.length

    if (!preMatch && i === 0) {
      strings.push(t.stringLiteral(''))
    } else {
      strings.push(t.stringLiteral(preMatch))
    }

    finalExpressions.push(expressions[p1])
    if (i === matches.length - 1) {
      strings.push(t.stringLiteral(str.substring(index + value.length)))
    }
  })

  return interleave(strings, finalExpressions).filter(
    (node /*: { value: string } */) => {
      return node.value !== ''
    }
  )
}

function createRawStringFromTemplateLiteral(
  quasi /*: {
  quasis: Array<{ value: { cooked: string } }>
} */
) {
  let strs = quasi.quasis.map(x => x.value.cooked)

  const src = strs
    .reduce((arr, str, i) => {
      arr.push(str)
      if (i !== strs.length - 1) {
        arr.push(`xxx${i}:xxx`)
      }
      return arr
    }, [])
    .join('')
    .trim()
  return src
}

export default function minify(path, t) {
  const quasi = path.node.quasi
  const raw = createRawStringFromTemplateLiteral(quasi)
  const minified = stringifyTree(toInputTree(compile(raw), []))
  const expressions = replacePlaceholdersWithExpressions(
    minified,
    quasi.expressions || [],
    t
  )
  path.replaceWith(t.callExpression(path.node.tag, expressions))
}