/
index.js
116 lines (93 loc) · 3.34 KB
/
index.js
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
import difference from 'lodash/difference'
import {
makePlaceholder,
placeholderRegex,
splitByPlaceholders,
} from '../css/placeholderUtils'
const injectUniquePlaceholders = strArr => {
let i = 0
return strArr.reduce((str, val, index, arr) => {
return str + val + (index < arr.length - 1 ? makePlaceholder(i++) : '')
}, '')
}
const makeMultilineCommentRegex = newlinePattern =>
new RegExp('\\/\\*[^!](.|' + newlinePattern + ')*?\\*\\/', 'g')
const lineCommentStart = /\/\//g
const symbolRegex = /(\s*[;:{},]\s*)/g
// Counts occurences of substr inside str
const countOccurences = (str, substr) => str.split(substr).length - 1
// Joins substrings until predicate returns true
const reduceSubstr = (substrs, join, predicate) => {
const length = substrs.length
let res = substrs[0]
if (length === 1) {
return res
}
for (let i = 1; i < length; i++) {
if (predicate(res)) {
break
}
res += join + substrs[i]
}
return res
}
// Joins at comment starts when it's inside a string or parantheses
// effectively removing line comments
export const stripLineComment = line =>
reduceSubstr(
line.split(lineCommentStart),
'//',
str =>
!str.endsWith(':') && // NOTE: This is another guard against urls, if they're not inside strings or parantheses.
countOccurences(str, "'") % 2 === 0 &&
countOccurences(str, '"') % 2 === 0 &&
countOccurences(str, '(') === countOccurences(str, ')')
)
export const compressSymbols = code =>
code.split(symbolRegex).reduce((str, fragment, index) => {
// Even-indices are non-symbol fragments
if (index % 2 === 0) {
return str + fragment
}
// Only manipulate symbols outside of strings
if (
countOccurences(str, "'") % 2 !== 0 ||
countOccurences(str, '"') % 2 !== 0
) {
return str + fragment
}
// Preserve whitespace preceding colon, to avoid joining selectors.
if (/^\s+:/.test(fragment)) {
return str + ' ' + fragment.trim()
}
return str + fragment.trim()
}, '')
// Detects lines that are exclusively line comments
const isLineComment = line => line.trim().startsWith('//')
// Creates a minifier with a certain linebreak pattern
const minify = linebreakPattern => {
const linebreakRegex = new RegExp(linebreakPattern + '\\s*', 'g')
const multilineCommentRegex = makeMultilineCommentRegex(linebreakPattern)
return code => {
const newCode = code
.replace(multilineCommentRegex, '\n') // Remove multiline comments
.split(linebreakRegex) // Split at newlines
.filter(line => line.length > 0 && !isLineComment(line)) // Removes lines containing only line comments
.map(stripLineComment) // Remove line comments inside text
.join(' ') // Rejoin all lines
const eliminatedExpressionIndices = difference(
code.match(placeholderRegex),
newCode.match(placeholderRegex)
).map(x => parseInt(x.match(/\d+/)[0], 10))
return [compressSymbols(newCode), eliminatedExpressionIndices]
}
}
export const minifyRaw = minify('(?:\\\\r|\\\\n|\\r|\\n)')
export const minifyCooked = minify('[\\r\\n]')
export const minifyRawValues = rawValues =>
splitByPlaceholders(minifyRaw(injectUniquePlaceholders(rawValues)), false)
export const minifyCookedValues = cookedValues =>
splitByPlaceholders(
minifyCooked(injectUniquePlaceholders(cookedValues)),
false
)