rastrillo / fiesta Public

Clone
git clone https://amadan.net/rastrillo/fiesta

Plain git — no account needed to clone.

Download

Download this file

1package build
2
3import (
4 "bytes"
5 "compress/gzip"
6 "strings"
7)
8
9// The comment strippers the release build runs over every stylesheet
10// and script, and the compressor the size gate measures with. Both
11// come from rastrillo's internal/designsystem/weight.go, where they
12// computed the figures on its Getting started page; here what they
13// produce is what ships, so the stripped output is held to being the
14// same stylesheet and the same script, not only a smaller one.
15
16// gzipLevel is the level every figure is compressed at: 6, gzip's own
17// default and the level a server or edge that compresses on the fly
18// typically uses. A fixed level, so the figure does not depend on
19// whatever DefaultCompression happens to mean in a given Go release.
20const gzipLevel = 6
21
22// gzipped is the compressed length of b at gzipLevel.
23func gzipped(b []byte) int {
24 var buf bytes.Buffer
25 zw, err := gzip.NewWriterLevel(&buf, gzipLevel)
26 if err != nil {
27 // Only an out-of-range level errors, and gzipLevel is a constant
28 // inside the range.
29 panic("build: " + err.Error())
30 }
31 zw.Write(b)
32 zw.Close()
33 return buf.Len()
34}
35
36// stripCSSComments removes every /* */ comment outside a string. A
37// comment on a line of its own takes the line with it, so the file is
38// one with no comments, not one full of blank lines.
39func stripCSSComments(src []byte) []byte {
40 var out bytes.Buffer
41 for i := 0; i < len(src); {
42 c := src[i]
43 switch {
44 case c == '"' || c == '\'':
45 j := endOfQuoted(src, i)
46 out.Write(src[i:j])
47 i = j
48 case c == '/' && i+1 < len(src) && src[i+1] == '*':
49 i = dropComment(&out, src, i, blockCommentEnd(src, i))
50 default:
51 out.WriteByte(c)
52 i++
53 }
54 }
55 return out.Bytes()
56}
57
58// stripJSComments removes every // and /* */ comment from a script while
59// leaving everything that only looks like one alone: a // inside a
60// string, a /* inside a regular expression, a comment-shaped run inside
61// a template literal and the code inside that literal's ${}. That is
62// three lexical states plus the one ambiguity JavaScript is famous for,
63// whether a / starts a regular expression or divides; see regexAllowed.
64//
65// The result is served, so it is held to being a script a browser
66// parses to the same program: TestStrippedScriptsEvaluateAsWritten and
67// TestEveryShippedScriptStripsToTheSameShape run both halves in Node.
68func stripJSComments(src []byte) []byte {
69 var out bytes.Buffer
70 // braces is one entry per open { since the innermost template
71 // literal's ${: when it reaches zero on a }, that } closes the ${
72 // and the scan is back inside the literal.
73 var braces []int
74 // parens is one entry per open (, true where it opened the head of
75 // an if, while, for or with. A / after the ) closing one of those
76 // starts the statement it governs, so it is a regex: if (x) /re/.
77 // After any other ) it divides: (a + b) / 2. ctlEnd is the length
78 // of out just past the last ) that closed such a head.
79 var parens []bool
80 ctlEnd := -1
81 for i := 0; i < len(src); {
82 c := src[i]
83 switch {
84 case c == '(':
85 parens = append(parens, controlHead(out.Bytes()))
86 out.WriteByte(c)
87 i++
88 case c == ')':
89 head := false
90 if n := len(parens); n > 0 {
91 head, parens = parens[n-1], parens[:n-1]
92 }
93 out.WriteByte(c)
94 i++
95 if head {
96 ctlEnd = out.Len()
97 }
98 case c == '"' || c == '\'':
99 j := endOfQuoted(src, i)
100 out.Write(src[i:j])
101 i = j
102 case c == '`':
103 j, opened := endOfTemplate(src, i+1)
104 out.Write(src[i:j])
105 i = j
106 if opened {
107 braces = append(braces, 0)
108 }
109 case c == '{' && len(braces) > 0:
110 braces[len(braces)-1]++
111 out.WriteByte(c)
112 i++
113 case c == '}' && len(braces) > 0 && braces[len(braces)-1] == 0:
114 braces = braces[:len(braces)-1]
115 j, opened := endOfTemplate(src, i+1)
116 out.Write(src[i:j])
117 i = j
118 if opened {
119 braces = append(braces, 0)
120 }
121 case c == '}' && len(braces) > 0:
122 braces[len(braces)-1]--
123 out.WriteByte(c)
124 i++
125 case c == '/' && i+1 < len(src) && src[i+1] == '/':
126 end := i + 2
127 for end < len(src) && src[end] != '\n' {
128 end++
129 }
130 i = dropComment(&out, src, i, end)
131 case c == '/' && i+1 < len(src) && src[i+1] == '*':
132 i = dropComment(&out, src, i, blockCommentEnd(src, i))
133 case c == '/' && regexAllowed(out.Bytes(), ctlEnd):
134 j := endOfRegex(src, i)
135 out.Write(src[i:j])
136 i = j
137 default:
138 out.WriteByte(c)
139 i++
140 }
141 }
142 return out.Bytes()
143}
144
145// dropComment skips src[start:end], a comment, and returns where the
146// scan resumes. A comment alone on its line takes the line with it. A
147// block comment spanning lines inside code leaves a newline behind
148// rather than nothing: JavaScript's automatic semicolons read line
149// breaks, and a comment that held two statements apart must still. One
150// inside a line leaves a space unless a space is already beside it: a
151// comment separates tokens, and with nothing in its place a+/**/++b
152// would read a++ +b, a different sum that parses just as well.
153func dropComment(out *bytes.Buffer, src []byte, start, end int) int {
154 b := out.Bytes()
155 lineStart := bytes.LastIndexByte(b, '\n') + 1
156 aloneBefore := len(bytes.TrimLeft(b[lineStart:], " \t")) == 0
157 rest := end
158 for rest < len(src) && (src[rest] == ' ' || src[rest] == '\t' || src[rest] == '\r') {
159 rest++
160 }
161 aloneAfter := rest == len(src) || src[rest] == '\n'
162 if aloneBefore && aloneAfter {
163 out.Truncate(lineStart)
164 if rest < len(src) {
165 rest++ // the line's own newline
166 }
167 return rest
168 }
169 if bytes.IndexByte(src[start:end], '\n') >= 0 {
170 out.WriteByte('\n')
171 } else if end < len(src) && !isSpaceByte(src[end]) && len(b) > 0 && !isSpaceByte(b[len(b)-1]) {
172 out.WriteByte(' ')
173 }
174 return end
175}
176
177// blockCommentEnd is the index just past the */ closing the comment
178// that starts at i, or the end of src for one that never closes.
179func blockCommentEnd(src []byte, i int) int {
180 if j := bytes.Index(src[i+2:], []byte("*/")); j >= 0 {
181 return i + 2 + j + 2
182 }
183 return len(src)
184}
185
186// endOfQuoted is the index just past the string literal opening at i,
187// honouring backslash escapes.
188func endOfQuoted(src []byte, i int) int {
189 q := src[i]
190 for j := i + 1; j < len(src); j++ {
191 switch src[j] {
192 case '\\':
193 j++
194 case q:
195 return j + 1
196 }
197 }
198 return len(src)
199}
200
201// endOfTemplate scans template-literal text from i and returns the
202// index just past where it stops: past the closing backtick, or past a
203// ${, in which case opened is true and code follows.
204func endOfTemplate(src []byte, i int) (int, bool) {
205 for j := i; j < len(src); j++ {
206 switch src[j] {
207 case '\\':
208 j++
209 case '`':
210 return j + 1, false
211 case '$':
212 if j+1 < len(src) && src[j+1] == '{' {
213 return j + 2, true
214 }
215 }
216 }
217 return len(src), false
218}
219
220// endOfRegex is the index just past the regular-expression literal
221// opening at i, flags included. A / inside a [class] does not close it.
222func endOfRegex(src []byte, i int) int {
223 inClass := false
224 for j := i + 1; j < len(src); j++ {
225 switch c := src[j]; {
226 case c == '\\':
227 j++
228 case c == '\n':
229 return j // not a regex after all; give the line back
230 case c == '[':
231 inClass = true
232 case c == ']':
233 inClass = false
234 case c == '/' && !inClass:
235 j++
236 for j < len(src) && isWordByte(src[j]) {
237 j++
238 }
239 return j
240 }
241 }
242 return len(src)
243}
244
245// regexAllowed says whether a / after the code written so far starts a
246// regular expression rather than dividing. The rule is the usual one: a
247// regex can follow an operator, an opening bracket, a comma or a
248// semicolon, the ) of a control-flow head (ctlEnd, see stripJSComments),
249// or one of the keywords that take an expression; a division follows a
250// value: a name, a number, any other closing bracket, and x++ or x--.
251//
252// A } is taken for the end of an object, so a / after it divides. A
253// regex opening a statement straight after a block's } would be read
254// wrong; no shipped script writes one, and
255// TestEveryShippedScriptStripsToTheSameShape would say if one did.
256func regexAllowed(before []byte, ctlEnd int) bool {
257 b := bytes.TrimRight(before, " \t\r\n")
258 if len(b) == 0 {
259 return true
260 }
261 last := b[len(b)-1]
262 if last == ')' {
263 return len(b) == ctlEnd
264 }
265 if (last == '+' || last == '-') && len(b) > 1 && b[len(b)-2] == last {
266 return false
267 }
268 if strings.IndexByte("(,=:[!&|?{};+-*%<>~^", last) >= 0 {
269 return true
270 }
271 if !isWordByte(last) {
272 return false
273 }
274 switch lastWord(b) {
275 case "return", "typeof", "case", "do", "else", "in", "of", "new", "delete", "void", "throw", "instanceof", "yield", "await":
276 return true
277 }
278 return false
279}
280
281// controlHead says whether a ( written after before opens the head of
282// an if, while, for or with, including for await (…), whose head is
283// a for's with one more word in front of the parenthesis.
284func controlHead(before []byte) bool {
285 b := bytes.TrimRight(before, " \t\r\n")
286 w := lastWord(b)
287 if w == "await" {
288 w = lastWord(bytes.TrimRight(b[:len(b)-len(w)], " \t\r\n"))
289 return w == "for"
290 }
291 switch w {
292 case "if", "while", "for", "with":
293 return true
294 }
295 return false
296}
297
298// lastWord is the identifier or keyword b ends with, if it ends with one.
299func lastWord(b []byte) string {
300 j := len(b)
301 for j > 0 && isWordByte(b[j-1]) {
302 j--
303 }
304 return string(b[j:])
305}
306
307func isSpaceByte(c byte) bool { return c == ' ' || c == '\t' || c == '\r' || c == '\n' }
308
309func isWordByte(c byte) bool {
310 return c == '_' || c == '$' || c >= '0' && c <= '9' || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= 0x80
311}
312