JsoupCssInliner.java

  1. package fr.sii.ogham.html.inliner.impl.jsoup;

  2. import static fr.sii.ogham.core.util.HtmlUtils.getCssUrlFunctions;
  3. import static fr.sii.ogham.core.util.HtmlUtils.relativize;
  4. import static fr.sii.ogham.html.inliner.impl.jsoup.CssInlineUtils.isInlineModeAllowed;

  5. import java.util.List;
  6. import java.util.StringTokenizer;
  7. import java.util.regex.Pattern;

  8. import org.jsoup.Jsoup;
  9. import org.jsoup.nodes.DataNode;
  10. import org.jsoup.nodes.Document;
  11. import org.jsoup.nodes.Element;
  12. import org.jsoup.parser.Tag;
  13. import org.jsoup.select.Elements;
  14. import org.slf4j.Logger;
  15. import org.slf4j.LoggerFactory;

  16. import fr.sii.ogham.core.util.CssUrlFunction;
  17. import fr.sii.ogham.html.inliner.CssInliner;
  18. import fr.sii.ogham.html.inliner.CssInlinerConstants.InlineModes;
  19. import fr.sii.ogham.html.inliner.ExternalCss;

  20. public class JsoupCssInliner implements CssInliner {
  21.     private static final Logger LOG = LoggerFactory.getLogger(JsoupCssInliner.class);
  22.    
  23.     private static final String HREF_ATTR = "href";
  24.     private static final String TEMP_STYLE_ATTR = "data-cssstyle";
  25.     private static final String STYLE_ATTR = "style";
  26.     private static final String STYLE_TAG = "style";
  27.     private static final String CSS_LINKS_SELECTOR = "link[rel*=\"stylesheet\"], link[type=\"text/css\"], link[href$=\".css\"]";
  28.     private static final Pattern NEW_LINES = Pattern.compile("\n");
  29.     private static final Pattern COMMENTS = Pattern.compile("/\\*.*?\\*/");
  30.     private static final Pattern SPACES = Pattern.compile(" +");
  31.     private static final String QUOTE_ENTITY = """;

  32.     @Override
  33.     public String inline(String htmlContent, List<ExternalCss> cssContents) {
  34.         Document doc = Jsoup.parse(htmlContent);

  35.         internStyles(doc, cssContents);
  36.         String stylesheet = fetchStyles(doc);
  37.         extractStyles(doc, stylesheet);
  38.         applyStyles(doc);

  39.         return doc.outerHtml();
  40.     }

  41.     /**
  42.      * Applies the styles to a <code>data-cssstyle</code> attribute. This is
  43.      * because the styles need to be applied sequentially, but before the
  44.      * <code>style</code> defined for the element inline.
  45.      *
  46.      * @param doc
  47.      *            the html document
  48.      */
  49.     private static void extractStyles(Document doc, String stylesheet) {
  50.         String cleanedStylesheet = ignoreAtRules(stylesheet);
  51.         cleanedStylesheet = NEW_LINES.matcher(cleanedStylesheet).replaceAll("");
  52.         cleanedStylesheet = COMMENTS.matcher(cleanedStylesheet).replaceAll("");
  53.         cleanedStylesheet = SPACES.matcher(cleanedStylesheet).replaceAll(" ");
  54.         String styleRules = cleanedStylesheet.trim();
  55.         String delims = "{}";
  56.         StringTokenizer st = new StringTokenizer(styleRules, delims);
  57.         while (st.countTokens() > 1) {
  58.             String selector = st.nextToken();
  59.             String properties = st.nextToken();
  60.             Elements selectedElements = doc.select(selector.trim());
  61.             for (Element selElem : selectedElements) {
  62.                 String oldProperties = selElem.attr(TEMP_STYLE_ATTR);
  63.                 selElem.attr(TEMP_STYLE_ATTR, oldProperties.length() > 0 ? concatenateProperties(oldProperties, properties) : properties);
  64.             }
  65.         }
  66.     }
  67.    
  68.     /**
  69.      * Replace link tags with style tags in order to keep the same inclusion
  70.      * order
  71.      *
  72.      * @param doc
  73.      *            the html document
  74.      * @param cssContents
  75.      *            the list of external css files with their content
  76.      */
  77.     private static void internStyles(Document doc, List<ExternalCss> cssContents) {
  78.         Elements els = doc.select(CSS_LINKS_SELECTOR);
  79.         for (Element e : els) {
  80.             if (isInlineModeAllowed(e, InlineModes.STYLE_ATTR)) {
  81.                 String path = e.attr(HREF_ATTR);
  82.                 ExternalCss css = getCss(cssContents, path);
  83.                 if (css != null) {
  84.                     Element style = new Element(Tag.valueOf(STYLE_TAG), "");
  85.                     style.appendChild(new DataNode(getCssContent(css)));
  86.                     e.replaceWith(style);
  87.                 }
  88.             }
  89.         }
  90.     }

  91.     private static ExternalCss getCss(List<ExternalCss> cssContents, String path) {
  92.         for (ExternalCss css : cssContents) {
  93.             if (css.getPath().getOriginalPath().contains(path)) {
  94.                 return css;
  95.             }
  96.         }
  97.         return null;
  98.     }

  99.     /**
  100.      * Generates a stylesheet from an html document
  101.      *
  102.      * @param doc
  103.      *            the html document
  104.      * @return a string representing the stylesheet.
  105.      */
  106.     private static String fetchStyles(Document doc) {
  107.         Elements els = doc.select(STYLE_TAG);
  108.         StringBuilder styles = new StringBuilder();
  109.         for (Element e : els) {
  110.             if (isInlineModeAllowed(e, InlineModes.STYLE_ATTR)) {
  111.                 styles.append(e.data());
  112.                 e.remove();
  113.             }
  114.         }
  115.         return styles.toString();
  116.     }

  117.     /**
  118.      * Transfers styles from the <code>data-cssstyle</code> attribute to the
  119.      * <code>style</code> attribute.
  120.      *
  121.      * @param doc
  122.      *            the html document
  123.      */
  124.     private static void applyStyles(Document doc) {
  125.         Elements allStyledElements = doc.getElementsByAttribute(TEMP_STYLE_ATTR);

  126.         for (Element e : allStyledElements) {
  127.             if (isInlineModeAllowed(e, InlineModes.STYLE_ATTR)) {
  128.                 String newStyle = e.attr(TEMP_STYLE_ATTR);
  129.                 String oldStyle = e.attr(STYLE_ATTR);
  130.                 e.attr(STYLE_ATTR, (trimAll(newStyle) + ";" + trimAll(oldStyle)).replaceAll(";+", ";").trim());
  131.             }
  132.             e.removeAttr(TEMP_STYLE_ATTR);
  133.         }
  134.     }

  135.     private static String concatenateProperties(String oldProp, String newProp) {
  136.         String prop = oldProp;
  137.         if (!prop.endsWith(";")) {
  138.             prop += ";";
  139.         }
  140.         return trimAll(prop) + " " + trimAll(newProp) + ";";
  141.     }
  142.    
  143.     private static String trimAll(String str) {
  144.         return str.replaceAll("\\s+", " ").trim();
  145.     }
  146.    

  147.     private static String ignoreAtRules(String stylesheet) {
  148.         StringBuilder sb = new StringBuilder();
  149.         AtRuleParserContext ctx = new AtRuleParserContext();
  150.         for (int i=0 ; i<stylesheet.length() ; i++) {
  151.             char c = stylesheet.charAt(i);
  152.             updateLineNumberIfNewLine(ctx, c);
  153.             markAsStartOfAtRuleIfAtChar(ctx, c);
  154.             markAsStartOfNestedAtRuleIfAlreadyInAtRuleAndIsOpeningBracket(ctx, c);
  155.             markAsEndOfNestedAtRuleIfAlreadyInAtRuleAndIsClosingBracket(ctx, c);
  156.             if (ignoreAtRuleIfAtEndOfAtRule(ctx, c)) {
  157.                 continue;
  158.             }
  159.             updateStylesAndAtRuleContent(ctx, sb, c);
  160.         }
  161.         return sb.toString();
  162.     }

  163.     private static boolean ignoreAtRuleIfAtEndOfAtRule(AtRuleParserContext ctx, char c) {
  164.         if (ctx.inAtRule && !ctx.inNestedAtRule && c == ';') {
  165.             ctx.inAtRule = false;
  166.             LOG.warn("{} rule is not handled by JsoupCssInliner implementation. Line {}:'{}' is skipped", rulename(ctx.rule), ctx.startLineOfCurrentAtRule, ctx.rule);
  167.             return true;
  168.         }
  169.         if (ctx.inAtRule && ctx.inNestedAtRule && ctx.numberOfOpenedAtRules == 0) {
  170.             ctx.inAtRule = false;
  171.             ctx.inNestedAtRule = false;
  172.             LOG.warn("{} rule is not handled by JsoupCssInliner implementation. Lines {}-{} are skipped", rulename(ctx.rule), ctx.startLineOfCurrentAtRule, ctx.line);
  173.             return true;
  174.         }
  175.         return false;
  176.     }

  177.     private static void updateStylesAndAtRuleContent(AtRuleParserContext ctx, StringBuilder sb, char c) {
  178.         if (!ctx.inAtRule) {
  179.             sb.append(c);
  180.             ctx.rule = new StringBuilder();
  181.         } else {
  182.             ctx.rule.append(c);
  183.         }
  184.     }

  185.     private static void markAsEndOfNestedAtRuleIfAlreadyInAtRuleAndIsClosingBracket(AtRuleParserContext ctx, char c) {
  186.         if (ctx.inAtRule && ctx.inNestedAtRule && c == '}') {
  187.             ctx.numberOfOpenedAtRules--;
  188.         }
  189.     }

  190.     private static void markAsStartOfNestedAtRuleIfAlreadyInAtRuleAndIsOpeningBracket(AtRuleParserContext ctx, char c) {
  191.         if (ctx.inAtRule && c == '{') {
  192.             ctx.inNestedAtRule = true;
  193.             ctx.numberOfOpenedAtRules++;
  194.         }
  195.     }

  196.     private static void markAsStartOfAtRuleIfAtChar(AtRuleParserContext ctx, char c) {
  197.         if (c == '@' && !ctx.inAtRule) {
  198.             ctx.inAtRule = true;
  199.             ctx.startLineOfCurrentAtRule = ctx.line;
  200.         }
  201.     }

  202.     private static void updateLineNumberIfNewLine(AtRuleParserContext ctx, char c) {
  203.         if (c == '\n') {
  204.             ctx.line++;
  205.         }
  206.     }
  207.    
  208.     private static String rulename(StringBuilder rule) {
  209.         StringBuilder name = new StringBuilder();
  210.         for (int i=0 ; i<rule.length() ; i++) {
  211.             char c = rule.charAt(i);
  212.             if (c != '@' && c != '-' && !Character.isAlphabetic(c) && !Character.isDigit(c)) {
  213.                 break;
  214.             }
  215.             name.append(c);
  216.         }
  217.         return name.toString();
  218.     }


  219.     private static String getCssContent(ExternalCss css) {
  220.         String content = css.getContent();
  221.         return updateRelativeUrls(content, css);
  222.     }

  223.     private static String updateRelativeUrls(String content, ExternalCss css) {
  224.         String newContent = content;
  225.         for (CssUrlFunction match : getCssUrlFunctions(content, QUOTE_ENTITY)) {
  226.             newContent = match.rewriteUrl(newContent, relativize(css.getPath().getOriginalPath(), match.getUrl()));
  227.         }
  228.         return newContent;
  229.     }

  230.     private static class AtRuleParserContext {
  231.         protected int line;
  232.         protected int startLineOfCurrentAtRule;
  233.         protected boolean inAtRule;
  234.         protected boolean inNestedAtRule;
  235.         protected int numberOfOpenedAtRules;
  236.         protected StringBuilder rule;
  237.        
  238.         public AtRuleParserContext() {
  239.             super();
  240.             this.line = 1;
  241.             this.startLineOfCurrentAtRule = 0;
  242.             this.inAtRule = false;
  243.             this.inNestedAtRule = false;
  244.             this.numberOfOpenedAtRules = 0;
  245.             this.rule = new StringBuilder();
  246.         }
  247.        
  248.        
  249.     }
  250. }