EMMA Coverage Report (generated Mon Nov 01 16:48:29 PDT 2010)
[all classes][com.google.caja.ancillary.servlet]

COVERAGE SUMMARY FOR SOURCE FILE [Processor.java]

nameclass, %method, %block, %line, %
Processor.java100% (4/4)100% (32/32)84%  (1958/2320)85%  (380.9/449)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class Processor100% (1/1)100% (27/27)83%  (1732/2086)84%  (354.1/422)
<static initializer> 100% (1/1)100% (37/37)100% (1/1)
Processor (Request, MessageQueue): void 100% (1/1)100% (9/9)100% (4/4)
access$000 (Processor, AncestorChain, CssValidator): void 100% (1/1)100% (5/5)100% (1/1)
clearAttributes (CssTree): void 100% (1/1)100% (25/25)100% (4/4)
cssExprParts (CssTree): List 100% (1/1)100% (13/13)100% (3/3)
cssExprPartsConsistent (List, List, String): boolean 100% (1/1)86%  (48/56)92%  (10.2/11)
doc (List, Request, MessageQueue): Job 100% (1/1)96%  (108/112)90%  (17.9/20)
extractJobs (Job): List 100% (1/1)100% (26/26)100% (4/4)
extractJobs (Node, URI, List): void 100% (1/1)91%  (97/107)81%  (17/21)
lint (List): void 100% (1/1)89%  (42/47)86%  (11.2/13)
lintCss (CssTree): void 100% (1/1)100% (18/18)100% (3/3)
lintJs (List): void 100% (1/1)100% (30/30)100% (6/6)
lintMarkup (Node): void 100% (1/1)64%  (79/123)82%  (14.8/18)
makeRenderContext (StringBuilder, ContentType): RenderContext 100% (1/1)91%  (80/88)94%  (17.9/19)
optimize (List): void 100% (1/1)97%  (36/37)91%  (10/11)
optimizeCss (Job): Job 100% (1/1)100% (31/31)100% (5/5)
optimizeCssDeclarations (AncestorChain, CssValidator): void 100% (1/1)84%  (186/222)87%  (44.1/51)
optimizeHtml (Job): Job 100% (1/1)100% (9/9)100% (3/3)
optimizeHtml (Node): void 100% (1/1)100% (108/108)100% (23/23)
optimizeJs (Job): Job 100% (1/1)100% (60/60)100% (11/11)
parse (CharProducer, ContentType, Node, URI): Job 100% (1/1)60%  (145/240)65%  (34/52)
process (List): List 100% (1/1)94%  (101/107)85%  (17/20)
propertyPrefix (Name): Name 100% (1/1)100% (17/17)100% (4/4)
reduce (List): Content 100% (1/1)65%  (235/364)65%  (45.6/70)
reincorporateExtracted (List): void 100% (1/1)100% (119/119)100% (27/27)
removeCajolerSpecificMessages (MessageQueue): void 100% (1/1)83%  (15/18)90%  (2.7/3)
withoutCssPartPrefix (String, String): String 100% (1/1)91%  (53/58)91%  (12.7/14)
     
class Processor$1100% (1/1)100% (2/2)100% (146/146)100% (23/23)
Processor$1 (Processor, CssValidator): void 100% (1/1)100% (9/9)100% (1/1)
visit (AncestorChain): boolean 100% (1/1)100% (137/137)100% (22/22)
     
class Processor$2100% (1/1)100% (2/2)100% (23/23)100% (4/4)
Processor$2 (List): void 100% (1/1)100% (6/6)100% (1/1)
visit (AncestorChain): boolean 100% (1/1)100% (17/17)100% (3/3)
     
class Processor$3100% (1/1)100% (1/1)88%  (57/65)88%  (1.8/2)
<static initializer> 100% (1/1)88%  (57/65)88%  (1.8/2)

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 
15package com.google.caja.ancillary.servlet;
16 
17import com.google.caja.SomethingWidgyHappenedError;
18import com.google.caja.ancillary.jsdoc.HtmlRenderer;
19import com.google.caja.ancillary.jsdoc.Jsdoc;
20import com.google.caja.ancillary.jsdoc.JsdocException;
21import com.google.caja.ancillary.linter.Linter;
22import com.google.caja.ancillary.opt.JsOptimizer;
23import com.google.caja.lang.css.CssSchema;
24import com.google.caja.lang.html.HTML;
25import com.google.caja.lexer.CharProducer;
26import com.google.caja.lexer.CssTokenType;
27import com.google.caja.lexer.FilePosition;
28import com.google.caja.lexer.HtmlLexer;
29import com.google.caja.lexer.HtmlTokenType;
30import com.google.caja.lexer.InputSource;
31import com.google.caja.lexer.JsLexer;
32import com.google.caja.lexer.JsTokenQueue;
33import com.google.caja.lexer.ParseException;
34import com.google.caja.lexer.Punctuation;
35import com.google.caja.lexer.Token;
36import com.google.caja.lexer.TokenConsumer;
37import com.google.caja.lexer.TokenQueue;
38import com.google.caja.lexer.escaping.UriUtil;
39import com.google.caja.parser.AncestorChain;
40import com.google.caja.parser.ParseTreeNode;
41import com.google.caja.parser.Visitor;
42import com.google.caja.parser.css.CssParser;
43import com.google.caja.parser.css.CssTree;
44import com.google.caja.parser.html.AttribKey;
45import com.google.caja.parser.html.DomParser;
46import com.google.caja.parser.html.ElKey;
47import com.google.caja.parser.html.HtmlQuasiBuilder;
48import com.google.caja.parser.html.Nodes;
49import com.google.caja.parser.js.Block;
50import com.google.caja.parser.js.Expression;
51import com.google.caja.parser.js.ObjectConstructor;
52import com.google.caja.parser.js.Parser;
53import com.google.caja.parser.js.Statement;
54import com.google.caja.plugin.CssPropertyPartType;
55import com.google.caja.plugin.CssRewriter;
56import com.google.caja.plugin.CssValidator;
57import com.google.caja.plugin.PluginMessageType;
58import com.google.caja.plugin.UriFetcher;
59import com.google.caja.plugin.stages.EmbeddedContent;
60import com.google.caja.plugin.stages.HtmlEmbeddedContentFinder;
61import com.google.caja.render.Concatenator;
62import com.google.caja.render.CssMinimalPrinter;
63import com.google.caja.render.CssPrettyPrinter;
64import com.google.caja.render.JsMinimalPrinter;
65import com.google.caja.render.JsPrettyPrinter;
66import com.google.caja.reporting.DevNullMessageQueue;
67import com.google.caja.reporting.MarkupRenderMode;
68import com.google.caja.reporting.Message;
69import com.google.caja.reporting.MessageLevel;
70import com.google.caja.reporting.MessagePart;
71import com.google.caja.reporting.MessageQueue;
72import com.google.caja.reporting.MessageTypeInt;
73import com.google.caja.reporting.RenderContext;
74import com.google.caja.util.ContentType;
75import com.google.caja.util.Lists;
76import com.google.caja.util.Multimap;
77import com.google.caja.util.Multimaps;
78import com.google.caja.util.Name;
79import com.google.caja.util.Sets;
80import com.google.caja.util.Strings;
81 
82import java.io.File;
83import java.io.IOException;
84import java.net.URI;
85import java.util.Collections;
86import java.util.Iterator;
87import java.util.List;
88import java.util.ListIterator;
89import java.util.Set;
90 
91import org.w3c.dom.Attr;
92import org.w3c.dom.DocumentFragment;
93import org.w3c.dom.Element;
94import org.w3c.dom.Node;
95import org.w3c.dom.Text;
96 
97/**
98 * The tools servlet's transformation engine.
99 *
100 * @author mikesamuel@gmail.com
101 */
102class 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}

[all classes][com.google.caja.ancillary.servlet]
EMMA 2.0.5312 (C) Vladimir Roubtsov