1 | // Copyright (C) 2009 Google Inc. |
2 | // |
3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
4 | // you may not use this file except in compliance with the License. |
5 | // You may obtain a copy of the License at |
6 | // |
7 | // http://www.apache.org/licenses/LICENSE-2.0 |
8 | // |
9 | // Unless required by applicable law or agreed to in writing, software |
10 | // distributed under the License is distributed on an "AS IS" BASIS, |
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
12 | // See the License for the specific language governing permissions and |
13 | // limitations under the License. |
14 | |
15 | package com.google.caja.ancillary.servlet; |
16 | |
17 | import com.google.caja.SomethingWidgyHappenedError; |
18 | import com.google.caja.ancillary.jsdoc.HtmlRenderer; |
19 | import com.google.caja.ancillary.jsdoc.Jsdoc; |
20 | import com.google.caja.ancillary.jsdoc.JsdocException; |
21 | import com.google.caja.ancillary.linter.Linter; |
22 | import com.google.caja.ancillary.opt.JsOptimizer; |
23 | import com.google.caja.lang.css.CssSchema; |
24 | import com.google.caja.lang.html.HTML; |
25 | import com.google.caja.lexer.CharProducer; |
26 | import com.google.caja.lexer.CssTokenType; |
27 | import com.google.caja.lexer.FilePosition; |
28 | import com.google.caja.lexer.HtmlLexer; |
29 | import com.google.caja.lexer.HtmlTokenType; |
30 | import com.google.caja.lexer.InputSource; |
31 | import com.google.caja.lexer.JsLexer; |
32 | import com.google.caja.lexer.JsTokenQueue; |
33 | import com.google.caja.lexer.ParseException; |
34 | import com.google.caja.lexer.Punctuation; |
35 | import com.google.caja.lexer.Token; |
36 | import com.google.caja.lexer.TokenConsumer; |
37 | import com.google.caja.lexer.TokenQueue; |
38 | import com.google.caja.lexer.escaping.UriUtil; |
39 | import com.google.caja.parser.AncestorChain; |
40 | import com.google.caja.parser.ParseTreeNode; |
41 | import com.google.caja.parser.Visitor; |
42 | import com.google.caja.parser.css.CssParser; |
43 | import com.google.caja.parser.css.CssTree; |
44 | import com.google.caja.parser.html.AttribKey; |
45 | import com.google.caja.parser.html.DomParser; |
46 | import com.google.caja.parser.html.ElKey; |
47 | import com.google.caja.parser.html.HtmlQuasiBuilder; |
48 | import com.google.caja.parser.html.Nodes; |
49 | import com.google.caja.parser.js.Block; |
50 | import com.google.caja.parser.js.Expression; |
51 | import com.google.caja.parser.js.ObjectConstructor; |
52 | import com.google.caja.parser.js.Parser; |
53 | import com.google.caja.parser.js.Statement; |
54 | import com.google.caja.plugin.CssPropertyPartType; |
55 | import com.google.caja.plugin.CssRewriter; |
56 | import com.google.caja.plugin.CssValidator; |
57 | import com.google.caja.plugin.PluginMessageType; |
58 | import com.google.caja.plugin.UriFetcher; |
59 | import com.google.caja.plugin.stages.EmbeddedContent; |
60 | import com.google.caja.plugin.stages.HtmlEmbeddedContentFinder; |
61 | import com.google.caja.render.Concatenator; |
62 | import com.google.caja.render.CssMinimalPrinter; |
63 | import com.google.caja.render.CssPrettyPrinter; |
64 | import com.google.caja.render.JsMinimalPrinter; |
65 | import com.google.caja.render.JsPrettyPrinter; |
66 | import com.google.caja.reporting.DevNullMessageQueue; |
67 | import com.google.caja.reporting.MarkupRenderMode; |
68 | import com.google.caja.reporting.Message; |
69 | import com.google.caja.reporting.MessageLevel; |
70 | import com.google.caja.reporting.MessagePart; |
71 | import com.google.caja.reporting.MessageQueue; |
72 | import com.google.caja.reporting.MessageTypeInt; |
73 | import com.google.caja.reporting.RenderContext; |
74 | import com.google.caja.util.ContentType; |
75 | import com.google.caja.util.Lists; |
76 | import com.google.caja.util.Multimap; |
77 | import com.google.caja.util.Multimaps; |
78 | import com.google.caja.util.Name; |
79 | import com.google.caja.util.Sets; |
80 | import com.google.caja.util.Strings; |
81 | |
82 | import java.io.File; |
83 | import java.io.IOException; |
84 | import java.net.URI; |
85 | import java.util.Collections; |
86 | import java.util.Iterator; |
87 | import java.util.List; |
88 | import java.util.ListIterator; |
89 | import java.util.Set; |
90 | |
91 | import org.w3c.dom.Attr; |
92 | import org.w3c.dom.DocumentFragment; |
93 | import org.w3c.dom.Element; |
94 | import org.w3c.dom.Node; |
95 | import org.w3c.dom.Text; |
96 | |
97 | /** |
98 | * The tools servlet's transformation engine. |
99 | * |
100 | * @author mikesamuel@gmail.com |
101 | */ |
102 | class Processor { |
103 | private final Request req; |
104 | private final MessageQueue mq; |
105 | |
106 | Processor(Request req, MessageQueue mq) { |
107 | this.req = req; |
108 | this.mq = mq; |
109 | } |
110 | |
111 | /** Produce a list of output jobs from a list of input jobs. */ |
112 | List<Job> process(List<Job> inputJobs) throws IOException { |
113 | List<Job> jobs = Lists.newArrayList(); |
114 | // Pull JS out of <script> elements, and similarly for style, and pull |
115 | // JS and CSS out of onclick and style attributes. |
116 | for (Job job : inputJobs) { |
117 | jobs.addAll(extractJobs(job)); |
118 | } |
119 | |
120 | if (req.lint) { lint(jobs); } |
121 | |
122 | if (req.opt) { optimize(jobs); } |
123 | |
124 | // Reverse of extractJobs. |
125 | // Put optimized JS back into the script element from which it came, and |
126 | // similarly for style elements and attributes. |
127 | if (req.minify || req.opt) { |
128 | reincorporateExtracted(jobs); |
129 | } |
130 | |
131 | List<Job> output = Lists.newArrayList(); |
132 | switch (req.verb) { |
133 | case DOC: |
134 | try { |
135 | output.add(doc(jobs, req, mq)); |
136 | } catch (JsdocException ex) { |
137 | ex.toMessageQueue(mq); |
138 | } |
139 | break; |
140 | case LINT: |
141 | output.add(Job.html(LintPage.render(reduce(jobs), req, mq), null)); |
142 | break; |
143 | default: |
144 | for (Job job : jobs) { |
145 | if (job.origin == null) { output.add(job); } |
146 | } |
147 | break; |
148 | } |
149 | |
150 | // Filter out some messages from the CssValidator and other linty bits. |
151 | removeCajolerSpecificMessages(mq); |
152 | |
153 | return output; |
154 | } |
155 | |
156 | /** |
157 | * Boil multiple jobs down into a single output. This may involve |
158 | * concatenating jobs of the same type or combining heterogeneous types into |
159 | * a single HTML file. |
160 | */ |
161 | Content reduce(List<Job> jobs) { |
162 | ContentType otype = req.otype; |
163 | if (otype == null) { // Guess if none was specified. |
164 | ContentType commonType = null; |
165 | for (Job job : jobs) { |
166 | if (commonType == null) { |
167 | commonType = job.t; |
168 | } else if (commonType != job.t) { |
169 | commonType = null; |
170 | break; |
171 | } |
172 | } |
173 | otype = commonType != null ? commonType : ContentType.HTML; |
174 | } |
175 | |
176 | // Do we need to combine everything into a single HTML file. |
177 | if (otype != ContentType.XML && otype != ContentType.HTML) { |
178 | for (Job job : jobs) { |
179 | if (job.t != otype) { |
180 | mq.addMessage( |
181 | CajaWebToolsMessageType.INCOMPATIBLE_OUTPUT_TYPE, |
182 | MessagePart.Factory.valueOf(job.t.name()), |
183 | MessagePart.Factory.valueOf(otype.name())); |
184 | otype = ContentType.HTML; |
185 | break; |
186 | } |
187 | } |
188 | } |
189 | |
190 | // If the output is not textual, we're done. |
191 | if (!otype.isText) { |
192 | if (jobs.size() != 1) { throw new AssertionError(); } |
193 | return new Content((byte[]) jobs.get(0).root, otype); |
194 | } |
195 | |
196 | // Format each of the jobs using the preferences in Request. |
197 | StringBuilder outBuf = new StringBuilder(); |
198 | RenderContext out = makeRenderContext(outBuf, otype); |
199 | switch (otype) { |
200 | case XML: |
201 | case HTML: |
202 | HtmlQuasiBuilder b = HtmlQuasiBuilder.getBuilder( |
203 | DomParser.makeDocument(null, null)); |
204 | DocumentFragment f = b.getDocument().createDocumentFragment(); |
205 | for (Job job : jobs) { |
206 | Node toAdd; |
207 | switch (job.t) { |
208 | case XML: case HTML: |
209 | toAdd = f.getOwnerDocument().importNode( |
210 | (DocumentFragment) job.root, true); |
211 | break; |
212 | case JS: { |
213 | StringBuilder sb = new StringBuilder(); |
214 | RenderContext rc = makeRenderContext(sb, ContentType.JS) |
215 | .withEmbeddable(true); |
216 | ((Block) job.root).renderBody(rc); |
217 | rc.getOut().noMoreTokens(); |
218 | toAdd = b.substV("<script>@js</script>", |
219 | "js", sb.toString()); |
220 | break; |
221 | } |
222 | case CSS: { |
223 | StringBuilder sb = new StringBuilder(); |
224 | RenderContext rc = makeRenderContext(sb, ContentType.CSS) |
225 | .withEmbeddable(true); |
226 | ((CssTree.StyleSheet) job.root).render(rc); |
227 | rc.getOut().noMoreTokens(); |
228 | toAdd = b.substV("<style>", |
229 | "css", sb.toString()); |
230 | break; |
231 | } |
232 | default: |
233 | throw new AssertionError(job.t.name()); |
234 | } |
235 | if (toAdd instanceof DocumentFragment) { |
236 | for (Node child : Nodes.childrenOf(toAdd)) { |
237 | f.appendChild(child); |
238 | } |
239 | } else { |
240 | f.appendChild(toAdd); |
241 | } |
242 | } |
243 | Nodes.render(f, out); |
244 | if (otype == ContentType.HTML && req.minify) { |
245 | out.getOut().noMoreTokens(); |
246 | String html = outBuf.toString(); |
247 | outBuf.setLength(0); |
248 | try { |
249 | HtmlReducer.reduce(html, outBuf); |
250 | } catch (ParseException ex) { |
251 | outBuf.setLength(0); |
252 | outBuf.append(html); |
253 | } |
254 | } |
255 | break; |
256 | case JS: |
257 | List<Statement> stmts = Lists.newArrayList(); |
258 | for (Job job : jobs) { stmts.addAll(((Block) job.root).children()); } |
259 | new Block(FilePosition.UNKNOWN, stmts).renderBody(out); |
260 | break; |
261 | case JSON: |
262 | for (Job job : jobs) { |
263 | ((Expression) job.root).render(out); |
264 | } |
265 | break; |
266 | case CSS: |
267 | for (Job job : jobs) { |
268 | ((CssTree.StyleSheet) job.root).render(out); |
269 | } |
270 | break; |
271 | case ZIP: default: throw new AssertionError(otype.name()); |
272 | } |
273 | out.getOut().noMoreTokens(); |
274 | return new Content(outBuf.toString(), otype); |
275 | } |
276 | |
277 | /** Parse a job from input parameters. */ |
278 | Job parse(CharProducer cp, ContentType contentType, Node src, URI baseUri) |
279 | throws ParseException { |
280 | FilePosition inputRange = cp.filePositionForOffsets( |
281 | cp.getOffset(), cp.getLimit()); |
282 | InputSource is = inputRange.source(); |
283 | switch (contentType) { |
284 | case HTML: |
285 | case XML: { |
286 | HtmlLexer lexer = new HtmlLexer(cp.clone()); |
287 | DomParser p; |
288 | if (contentType == ContentType.HTML) { |
289 | Token<HtmlTokenType> firstTag = null; |
290 | while (lexer.hasNext()) { |
291 | Token<HtmlTokenType> t = lexer.next(); |
292 | if (t.type == HtmlTokenType.TAGBEGIN) { |
293 | firstTag = t; |
294 | break; |
295 | } |
296 | } |
297 | p = new DomParser(new HtmlLexer(cp), false, is, mq); |
298 | if (firstTag != null |
299 | && Strings.equalsIgnoreCase(firstTag.text, "<html")) { |
300 | Element el = p.parseDocument(); |
301 | DocumentFragment f = el.getOwnerDocument().createDocumentFragment(); |
302 | f.appendChild(el); |
303 | return Job.html(f, baseUri); |
304 | } |
305 | } else { |
306 | lexer.setTreatedAsXml(contentType == ContentType.XML); |
307 | TokenQueue<HtmlTokenType> tq = new TokenQueue<HtmlTokenType>( |
308 | lexer, is, DomParser.SKIP_COMMENTS); |
309 | tq.setInputRange(inputRange); |
310 | p = new DomParser(tq, contentType == ContentType.XML, mq); |
311 | } |
312 | return Job.html(p.parseFragment(), baseUri); |
313 | } |
314 | case JS: { |
315 | JsLexer lexer = new JsLexer(cp); |
316 | JsTokenQueue tq = new JsTokenQueue(lexer, is); |
317 | if (tq.isEmpty()) { |
318 | return Job.js( |
319 | new Block(inputRange, Collections.<Statement>emptyList()), src, |
320 | baseUri); |
321 | } |
322 | tq.setInputRange(inputRange); |
323 | Block program = new Parser(tq, mq, false).parse(); |
324 | tq.expectEmpty(); |
325 | return Job.js(program, src, baseUri); |
326 | } |
327 | case JSON: { // TODO: use a JSON only lexer. |
328 | JsLexer lexer = new JsLexer(cp); |
329 | JsTokenQueue tq = new JsTokenQueue(lexer, is); |
330 | if (!tq.lookaheadToken(Punctuation.LCURLY)) { |
331 | tq.expectToken(Punctuation.LCURLY); |
332 | } |
333 | tq.setInputRange(inputRange); |
334 | Expression e = new Parser(tq, mq, false).parseExpressionPart(true); |
335 | tq.expectEmpty(); |
336 | return Job.json((ObjectConstructor) e, baseUri); |
337 | } |
338 | case CSS: { |
339 | TokenQueue<CssTokenType> tq = CssParser.makeTokenQueue(cp, mq, false); |
340 | tq.setInputRange(inputRange); |
341 | CssParser p = new CssParser(tq, mq, MessageLevel.WARNING); |
342 | Job job; |
343 | if (src instanceof Attr) { |
344 | CssTree.DeclarationGroup dg = p.parseDeclarationGroup(); |
345 | job = Job.css(dg, (Attr) src, baseUri); |
346 | } else { |
347 | CssTree.StyleSheet ss = p.parseStyleSheet(); |
348 | job = Job.css(ss, (Element) src, baseUri); // src may be null |
349 | } |
350 | tq.expectEmpty(); |
351 | return job; |
352 | } |
353 | default: |
354 | throw new AssertionError(contentType.name()); |
355 | } |
356 | } |
357 | |
358 | /** Make a renderer using the preferences specified in Request. */ |
359 | RenderContext makeRenderContext(StringBuilder out, ContentType ot) { |
360 | Concatenator cat = new Concatenator(out); |
361 | TokenConsumer tc; |
362 | switch (ot) { |
363 | case HTML: case XML: |
364 | tc = cat; |
365 | break; |
366 | case CSS: |
367 | if (req.minify) { |
368 | tc = new CssMinimalPrinter(cat); |
369 | } else { |
370 | tc = new CssPrettyPrinter(cat); |
371 | } |
372 | break; |
373 | case JS: |
374 | case JSON: |
375 | if (req.minify) { |
376 | tc = new JsMinimalPrinter(cat); |
377 | } else { |
378 | tc = new JsPrettyPrinter(cat); |
379 | } |
380 | break; |
381 | default: |
382 | throw new AssertionError(ot.name()); |
383 | } |
384 | RenderContext rc = new RenderContext(tc); |
385 | rc = rc.withMarkupRenderMode( |
386 | ot == ContentType.XML |
387 | ? MarkupRenderMode.XML : MarkupRenderMode.HTML); |
388 | rc = rc.withAsciiOnly(req.asciiOnly); |
389 | rc = rc.withJson(ot == ContentType.JSON); |
390 | rc = rc.withRawObjKeys(req.minify); |
391 | return rc; |
392 | } |
393 | |
394 | /** |
395 | * Pull the bodies of script and style elements out into their own jobs, |
396 | * and similarly for event handlers and style attributes. |
397 | */ |
398 | private List<Job> extractJobs(Job job) { |
399 | List<Job> all = Lists.newArrayList(job); |
400 | if (job.t == ContentType.XML || job.t == ContentType.HTML) { |
401 | extractJobs((Node) job.root, job.baseUri, all); |
402 | } |
403 | return all; |
404 | } |
405 | |
406 | private void extractJobs(Node node, URI baseUri, List<Job> out) { |
407 | HtmlEmbeddedContentFinder f = new HtmlEmbeddedContentFinder( |
408 | req.htmlSchema, req.baseUri, mq, req.mc); |
409 | for (EmbeddedContent c : f.findEmbeddedContent(node)) { |
410 | if (c.getType() != null && c.getContentLocation() == null) { |
411 | Node src = c.getSource(); |
412 | ParseTreeNode t; |
413 | try { |
414 | t = c.parse(UriFetcher.NULL_NETWORK, mq); |
415 | } catch (ParseException ex) { |
416 | ex.toMessageQueue(mq); |
417 | continue; |
418 | } |
419 | switch (c.getType()) { |
420 | case JS: |
421 | if (src instanceof Element) { |
422 | out.add(Job.js((Block) t, (Element) src, baseUri)); |
423 | } else { |
424 | out.add(Job.js((Block) t, (Attr) src, baseUri)); |
425 | } |
426 | break; |
427 | case CSS: |
428 | if (src instanceof Element) { |
429 | out.add(Job.css((CssTree.StyleSheet) t, (Element) src, baseUri)); |
430 | } else { |
431 | out.add(Job.css( |
432 | (CssTree.DeclarationGroup) t, (Attr) src, baseUri)); |
433 | } |
434 | break; |
435 | default: throw new SomethingWidgyHappenedError(); |
436 | } |
437 | } |
438 | } |
439 | } |
440 | |
441 | /** Find problems in code. */ |
442 | private void lint(List<Job> jobs) { |
443 | List<Block> jsJobs = Lists.newArrayList(); |
444 | for (Job job : jobs) { |
445 | switch (job.t) { |
446 | case XML: case HTML: |
447 | lintMarkup((DocumentFragment) job.root); |
448 | break; |
449 | case CSS: |
450 | lintCss((CssTree) job.root); |
451 | break; |
452 | case JS: |
453 | jsJobs.add((Block) job.root); |
454 | break; |
455 | case JSON: break; |
456 | case ZIP: throw new IllegalArgumentException(); |
457 | } |
458 | } |
459 | lintJs(jsJobs); |
460 | } |
461 | |
462 | private void lintMarkup(Node node) { |
463 | if (node instanceof Element) { |
464 | Element el = (Element) node; |
465 | ElKey elKey = ElKey.forElement(el); |
466 | HTML.Element elInfo = req.htmlSchema.lookupElement(elKey); |
467 | if (elInfo == null) { |
468 | mq.addMessage( |
469 | CajaWebToolsMessageType.UNKNOWN_ELEMENT, |
470 | Nodes.getFilePositionFor(el), elKey); |
471 | } |
472 | for (Attr a : Nodes.attributesOf(el)) { |
473 | AttribKey aKey = AttribKey.forAttribute(elKey, a); |
474 | HTML.Attribute aInfo = req.htmlSchema.lookupAttribute(aKey); |
475 | if (aInfo == null) { |
476 | FilePosition aPos = Nodes.getFilePositionFor(a); |
477 | mq.addMessage( |
478 | CajaWebToolsMessageType.UNKNOWN_ATTRIB, aPos, aKey, elKey); |
479 | } else if (!aInfo.getValueCriterion().accept(a.getValue())) { |
480 | FilePosition aPos = Nodes.getFilePositionForValue(a); |
481 | mq.addMessage( |
482 | CajaWebToolsMessageType.BAD_ATTRIB_VALUE, aPos, aKey, |
483 | MessagePart.Factory.valueOf(a.getValue())); |
484 | } |
485 | } |
486 | } |
487 | for (Node child : Nodes.childrenOf(node)) { lintMarkup(child); } |
488 | } |
489 | |
490 | private void lintCss(CssTree t) { |
491 | CssValidator v = new CssValidator(req.cssSchema, req.htmlSchema, mq); |
492 | v.validateCss(AncestorChain.instance(t)); |
493 | } |
494 | |
495 | private void lintJs(List<Block> programs) { |
496 | if (programs.isEmpty()) { return; } |
497 | List<Linter.LintJob> lintJobs = Lists.newArrayList(); |
498 | for (Block program : programs) { |
499 | lintJobs.add(Linter.makeLintJob(program, mq)); |
500 | } |
501 | Linter.lint(lintJobs, Linter.BROWSER_ENVIRONMENT, mq); // TODO: parameterize |
502 | } |
503 | |
504 | /** Replace jobs with more compact, semantically identical jobs. */ |
505 | private void optimize(List<Job> jobs) { |
506 | ListIterator<Job> jobIt = jobs.listIterator(); |
507 | while (jobIt.hasNext()) { |
508 | Job job = jobIt.next(); |
509 | switch (job.t) { |
510 | case JS: job = optimizeJs(job); break; |
511 | case HTML: job = optimizeHtml(job); break; |
512 | case CSS: job = optimizeCss(job); break; |
513 | default: continue; |
514 | } |
515 | jobIt.set(job); |
516 | } |
517 | } |
518 | |
519 | private Job optimizeJs(Job job) { |
520 | JsOptimizer opt = new JsOptimizer(mq); |
521 | opt.addInput((Block) job.root); |
522 | |
523 | ObjectConstructor envJson = req.userAgent != null |
524 | ? UserAgentDb.lookupEnvJson(req.userAgent) : null; |
525 | if (envJson == null) { |
526 | envJson = new ObjectConstructor(FilePosition.UNKNOWN); |
527 | } |
528 | opt.setEnvJson(envJson); |
529 | opt.setRename(true); |
530 | Statement optimized = opt.optimize(); |
531 | if (!(optimized instanceof Block)) { |
532 | optimized = new Block( |
533 | optimized.getFilePosition(), Collections.singletonList(optimized)); |
534 | } |
535 | return Job.js((Block) optimized, job.origin, job.baseUri); |
536 | } |
537 | |
538 | private Job optimizeHtml(Job job) { |
539 | DocumentFragment f = (DocumentFragment) job.root; |
540 | optimizeHtml(f); |
541 | return job; |
542 | } |
543 | |
544 | private Job optimizeCss(Job job) { |
545 | final CssValidator v = new CssValidator( |
546 | req.cssSchema, req.htmlSchema, DevNullMessageQueue.singleton()); |
547 | CssTree t = (CssTree) job.root; |
548 | v.validateCss(AncestorChain.instance(t)); |
549 | t.acceptPostOrder(new Visitor() { |
550 | public boolean visit(AncestorChain<?> ac) { |
551 | if (ac.node instanceof CssTree.RuleSet |
552 | || ac.node instanceof CssTree.DeclarationGroup) { |
553 | optimizeCssDeclarations(ac.cast(CssTree.class), v); |
554 | } else if (ac.node instanceof CssTree.IdentLiteral) { |
555 | Name part = ac.parent.node.getAttributes().get( |
556 | CssValidator.CSS_PROPERTY_PART); |
557 | if (part == null) { return true; } |
558 | String partS = part.getCanonicalForm(); |
559 | if ("color".equals(partS) || partS.endsWith("::color")) { |
560 | CssTree.IdentLiteral id = ac.cast(CssTree.IdentLiteral.class).node; |
561 | CssTree.HashLiteral hash = CssRewriter.colorHash( |
562 | id.getFilePosition(), Name.css(id.getValue())); |
563 | if (hash != null |
564 | && hash.getValue().length() < id.getValue().length()) { |
565 | hash.getAttributes().putAll(id.getAttributes()); |
566 | ((CssTree) ac.parent.node).replaceChild(hash, id); |
567 | } |
568 | } |
569 | } else if (ac.node instanceof CssTree.HashLiteral |
570 | && (ac.parent.node.getAttributes() |
571 | .get(CssValidator.CSS_PROPERTY_PART_TYPE) |
572 | == CssPropertyPartType.COLOR)) { |
573 | CssTree.HashLiteral hash = ac.cast(CssTree.HashLiteral.class).node; |
574 | String color = hash.getValue(); |
575 | if (color.length() == 7) { |
576 | int hex = Integer.valueOf(color.substring(1), 16); |
577 | CssTree.HashLiteral shortHash = CssRewriter.colorHash( |
578 | hash.getFilePosition(), hex); |
579 | if (shortHash.getValue().length() < hash.getValue().length()) { |
580 | shortHash.getAttributes().putAll(hash.getAttributes()); |
581 | ((CssTree) ac.parent.node).replaceChild(shortHash, hash); |
582 | } |
583 | } |
584 | } |
585 | return true; |
586 | } |
587 | }, null); |
588 | return job; |
589 | } |
590 | |
591 | private static Name propertyPrefix(Name propertyName) { |
592 | String canon = propertyName.getCanonicalForm(); |
593 | int dash = canon.lastIndexOf('-'); |
594 | if (dash < 0) { return null; } |
595 | return Name.css(canon.substring(0, dash)); |
596 | } |
597 | |
598 | private void optimizeCssDeclarations( |
599 | AncestorChain<? extends CssTree> cont, CssValidator v) { |
600 | List<CssTree.Declaration> decls = Lists.newArrayList(); |
601 | for (CssTree t : cont.node.children()) { |
602 | // RuleSets have non selectors too |
603 | if (!(t instanceof CssTree.Declaration)) { continue; } |
604 | decls.add((CssTree.Declaration) t); |
605 | } |
606 | // Maintain a prefix map so that we don't accidentally reduce two |
607 | // property names to the same prefix which would break them. |
608 | Multimap<Name, Name> propertyPrefixes = Multimaps.newListHashMultimap(); |
609 | for (Iterator<CssTree.Declaration> it = decls.iterator(); it.hasNext();) { |
610 | CssTree.Declaration d = it.next(); |
611 | if (d instanceof CssTree.EmptyDeclaration) { |
612 | cont.node.removeChild(d); |
613 | it.remove(); |
614 | } else if (d instanceof CssTree.PropertyDeclaration) { |
615 | CssTree.PropertyDeclaration pd = (CssTree.PropertyDeclaration) d; |
616 | Name propName = pd.getProperty().getPropertyName(); |
617 | for (Name n = propName; n != null; n = propertyPrefix(n)) { |
618 | propertyPrefixes.put(n, propName); |
619 | } |
620 | } else if (d instanceof CssTree.UserAgentHack) { |
621 | for (CssTree h : d.children()) { |
622 | CssTree.PropertyDeclaration pd = (CssTree.PropertyDeclaration) h; |
623 | Name propName = pd.getProperty().getPropertyName(); |
624 | for (Name n = propName; n != null; n = propertyPrefix(n)) { |
625 | propertyPrefixes.put(n, propName); |
626 | } |
627 | } |
628 | } |
629 | } |
630 | for (CssTree.Declaration d : decls) { |
631 | if (!(d instanceof CssTree.PropertyDeclaration)) { continue; } |
632 | CssTree.PropertyDeclaration pd = (CssTree.PropertyDeclaration) d; |
633 | CssTree.Property p = pd.getProperty(); |
634 | Name pName = p.getPropertyName(); |
635 | CssSchema.CssPropertyInfo i = req.cssSchema.getCssProperty(pName); |
636 | if (i == null) { continue; } |
637 | Name shortName = pName; |
638 | List<String> pExprTypes = null; |
639 | CssTree.Expr shortened = null; |
640 | for (Name prefix = shortName;(prefix = propertyPrefix(prefix)) != null;) { |
641 | if (propertyPrefixes.get(prefix).size() != 1) { break; } |
642 | CssSchema.CssPropertyInfo si = req.cssSchema.getCssProperty(prefix); |
643 | if (si == null) { break; } |
644 | // If we can shorten the name and get the same types out, then do so. |
645 | CssTree.Expr e = (CssTree.Expr) pd.getExpr().clone(); |
646 | clearAttributes(e); |
647 | if (!v.applySignature(prefix, e, si.sig)) { break; } |
648 | if (pExprTypes == null) { pExprTypes = cssExprParts(pd.getExpr()); } |
649 | if (!cssExprPartsConsistent( |
650 | cssExprParts(e), pExprTypes, pName.getCanonicalForm())) { |
651 | break; |
652 | } |
653 | shortName = prefix; |
654 | shortened = e; |
655 | } |
656 | if (shortName != pName) { |
657 | pd.replaceChild(shortened, pd.getExpr()); |
658 | pd.replaceChild( |
659 | new CssTree.Property(p.getFilePosition(), shortName), p); |
660 | } |
661 | } |
662 | } |
663 | |
664 | private static void clearAttributes(CssTree t) { |
665 | t.getAttributes().remove(CssValidator.CSS_PROPERTY_PART); |
666 | t.getAttributes().remove(CssValidator.CSS_PROPERTY_PART_TYPE); |
667 | for (CssTree c : t.children()) { clearAttributes(c); } |
668 | } |
669 | |
670 | private static List<String> cssExprParts(CssTree t) { |
671 | final List<String> out = Lists.newArrayList(); |
672 | t.acceptPostOrder(new Visitor() { |
673 | public boolean visit(AncestorChain<?> ac) { |
674 | Name part = ac.node.getAttributes().get(CssValidator.CSS_PROPERTY_PART); |
675 | if (part != null) { out.add(part.getCanonicalForm()); } |
676 | return true; |
677 | } |
678 | }, null); |
679 | return Collections.unmodifiableList(out); |
680 | } |
681 | |
682 | private static boolean cssExprPartsConsistent( |
683 | List<? extends String> a, List<? extends String> b, String toIgnore) { |
684 | if (toIgnore.startsWith("background-")) { |
685 | // Backgrounds layer instead of partitioning spatially |
686 | toIgnore = "background"; |
687 | } |
688 | int n = a.size(); |
689 | if (n != b.size()) { return false; } |
690 | for (int i = 0; i < n; ++i) { |
691 | String sa = a.get(i), sb = b.get(i); |
692 | if (sa == null) { return sb == null; } |
693 | if (sb == null) { return false; } |
694 | if (!withoutCssPartPrefix(sa, toIgnore).equals( |
695 | withoutCssPartPrefix(sb, toIgnore))) { |
696 | return false; |
697 | } |
698 | } |
699 | return true; |
700 | } |
701 | |
702 | private static String withoutCssPartPrefix(String part, String prefix) { |
703 | int n = part.length(), pn = prefix.length(); |
704 | int i = 0; |
705 | // :: is used to separate parts in a CssPropertyPart, as in |
706 | // background-color::color. |
707 | // For a prefix like "background", skip over any parts that match the prefix |
708 | // or that have (prefix + "-") as a prefix. |
709 | while (i < n) { |
710 | if (!part.regionMatches(i, prefix, 0, pn)) { break; } |
711 | int e = i + pn; |
712 | if (e != n) { |
713 | int dc = part.indexOf("::", e); |
714 | char ch = part.charAt(e); |
715 | // Skip over the part since '-' next establishes it is ignorable. |
716 | if (ch != '-' && e != dc) { break; } |
717 | i = dc < 0 ? n : dc + 2; // end of the next :: separator |
718 | } else { |
719 | i = e; |
720 | } |
721 | } |
722 | return part.substring(i); |
723 | } |
724 | |
725 | |
726 | private void optimizeHtml(Node n) { |
727 | if (n instanceof Element) { |
728 | Element el = (Element) n; |
729 | ElKey elKey = ElKey.forElement(el); |
730 | List<Attr> toRemove = Lists.newArrayList(); |
731 | for (Attr a : Nodes.attributesOf(el)) { |
732 | AttribKey aKey = AttribKey.forAttribute(elKey, a); |
733 | HTML.Attribute aInfo = req.htmlSchema.lookupAttribute(aKey); |
734 | if (aInfo != null && a.getValue().equals(aInfo.getDefaultValue())) { |
735 | toRemove.add(a); |
736 | } |
737 | } |
738 | for (Attr a : toRemove) { |
739 | el.removeAttributeNode(a); |
740 | } |
741 | HTML.Element elInfo = req.htmlSchema.lookupElement(elKey); |
742 | if (elInfo != null && !elInfo.canContainText()) { |
743 | for (Node c = el.getFirstChild(); c != null;) { |
744 | Node next = c.getNextSibling(); |
745 | if (c instanceof Text && "".equals(c.getNodeValue().trim())) { |
746 | el.removeChild(c); |
747 | } |
748 | c = next; |
749 | } |
750 | } |
751 | } |
752 | for (Node c = n.getFirstChild(); c != null; c = c.getNextSibling()) { |
753 | optimizeHtml(c); |
754 | } |
755 | } |
756 | |
757 | /** Instrument and run code to generate a documentation zip file. */ |
758 | private Job doc(List<Job> jobs, Request req, MessageQueue mq) |
759 | throws IOException, JsdocException { |
760 | Jsdoc jsdoc = new Jsdoc(req.mc, mq); |
761 | for (Job job : jobs) { |
762 | // Do not doc handlers. |
763 | if (job.t != ContentType.JS || job.origin instanceof Attr) { continue; } |
764 | Block program = (Block) job.root; |
765 | jsdoc.addSource(program); |
766 | } |
767 | try { |
768 | jsdoc.addInitFile( |
769 | "/js/jqueryjs/runtest/env.js", |
770 | "" + Resources.read( |
771 | CajaWebToolsServlet.class, "/js/jqueryjs/runtest/env.js") |
772 | ); |
773 | } catch (IOException ex) { |
774 | ex.printStackTrace(); |
775 | } |
776 | ObjectConstructor json = jsdoc.extract(); |
777 | if (req.otype == ContentType.JSON) { |
778 | return Job.json(json, null); |
779 | } else { |
780 | ZipFileSystem fs = new ZipFileSystem("/jsdoc"); |
781 | StringBuilder jsonSb = new StringBuilder(); |
782 | RenderContext rc = new RenderContext( |
783 | new JsMinimalPrinter(new Concatenator(jsonSb))).withJson(true); |
784 | json.render(rc); |
785 | rc.getOut().noMoreTokens(); |
786 | HtmlRenderer.buildHtml( |
787 | "" + jsonSb, fs, new File("/jsdoc"), req.srcMap.values(), |
788 | req.mc); |
789 | return fs.toZip(); |
790 | } |
791 | } |
792 | |
793 | /** The reversal of {@link #extractJobs}. */ |
794 | private void reincorporateExtracted(List<Job> jobs) { |
795 | Iterator<Job> jobsIt = jobs.iterator(); |
796 | while (jobsIt.hasNext()) { |
797 | Job job = jobsIt.next(); |
798 | if (job.origin == null) { continue; } |
799 | StringBuilder sb = new StringBuilder(); |
800 | RenderContext rc = makeRenderContext(sb, job.t).withEmbeddable(true); |
801 | if (job.root instanceof Block) { |
802 | ((Block) job.root).renderBody(rc); |
803 | } else { |
804 | ((ParseTreeNode) job.root).render(rc); |
805 | } |
806 | rc.getOut().noMoreTokens(); |
807 | String rendered = sb.toString(); |
808 | if (job.origin instanceof Element) { |
809 | Element origin = (Element) job.origin; |
810 | while (origin.getFirstChild() != null) { |
811 | origin.removeChild(origin.getFirstChild()); |
812 | } |
813 | origin.appendChild(origin.getOwnerDocument().createTextNode(rendered)); |
814 | jobsIt.remove(); |
815 | } else if (job.origin instanceof Attr) { |
816 | Attr origin = (Attr) job.origin; |
817 | if (job.t == ContentType.JS) { |
818 | HTML.Attribute aInfo = req.htmlSchema.lookupAttribute( |
819 | AttribKey.forAttribute( |
820 | ElKey.forElement(origin.getOwnerElement()), origin)); |
821 | if (aInfo != null && aInfo.getType() == HTML.Attribute.Type.URI) { |
822 | rendered = "javascript:" + UriUtil.encode(rendered); |
823 | } |
824 | } |
825 | origin.setNodeValue(rendered); |
826 | jobsIt.remove(); |
827 | } |
828 | } |
829 | } |
830 | |
831 | private static final Set<MessageTypeInt> IGNORED = Sets.immutableSet( |
832 | (MessageTypeInt) PluginMessageType.DISALLOWED_CSS_PROPERTY_IN_SELECTOR, |
833 | PluginMessageType.UNSAFE_CSS_PROPERTY, |
834 | PluginMessageType.UNSAFE_TAG, |
835 | PluginMessageType.CSS_ATTRIBUTE_TYPE_NOT_ALLOWED_IN_SELECTOR, |
836 | PluginMessageType.CSS_ATTRIBUTE_NAME_NOT_ALLOWED_IN_SELECTOR, |
837 | PluginMessageType.CSS_DASHMATCH_ATTRIBUTE_OPERATOR_NOT_ALLOWED, |
838 | PluginMessageType.IMPORTS_NOT_ALLOWED_HERE, |
839 | PluginMessageType.FONT_FACE_NOT_ALLOWED |
840 | ); |
841 | |
842 | private static void removeCajolerSpecificMessages(MessageQueue mq) { |
843 | for (Iterator<Message> i = mq.getMessages().iterator(); i.hasNext();) { |
844 | if (IGNORED.contains(i.next().getMessageType())) { i.remove(); } |
845 | } |
846 | } |
847 | } |