开发者

Automatically convert Style Sheets to inline style

Don't have to worry about linked style or hover style.

I want to automatically convert files like this

<html>
<body>
<style>
body{background:#FFC}
p{background:red}
body, p{font-weight:bold}
</style>
<p>...</p>
</body>
</html>

to files like this

<html>
<body style="background:red;font-weight:bold">
<p style="background:#FFC;font-weight:bold">...</p>
</body>
</html>
开发者_Go百科

I would be even more interested if there was an HTML parser that would do this.

The reason I want to do this is so I can display emails that use global style sheets without their style sheets messing up the rest of my web page. I also would like to send the resulting style to web based rich text editor for reply and original message.


Here is a solution on java, I made it with the JSoup Library: http://jsoup.org/download

import java.io.IOException;
import java.util.StringTokenizer;

import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;

public class AutomaticCssInliner {
    /**
     * Hecho por Grekz, http://grekz.wordpress.com
     */
    public static void main(String[] args) throws IOException {
        final String style = "style";
        final String html = "<html>" + "<body> <style>"
                + "body{background:#FFC} \n p{background:red}"
                + "body, p{font-weight:bold} </style>"
                + "<p>...</p> </body> </html>";
        // Document doc = Jsoup.connect("http://mypage.com/inlineme.php").get();
        Document doc = Jsoup.parse(html);
        Elements els = doc.select(style);// to get all the style elements
        for (Element e : els) {
            String styleRules = e.getAllElements().get(0).data().replaceAll(
                    "\n", "").trim(), delims = "{}";
            StringTokenizer st = new StringTokenizer(styleRules, delims);
            while (st.countTokens() > 1) {
                String selector = st.nextToken(), properties = st.nextToken();
                Elements selectedElements = doc.select(selector);
                for (Element selElem : selectedElements) {
                    String oldProperties = selElem.attr(style);
                    selElem.attr(style,
                            oldProperties.length() > 0 ? concatenateProperties(
                                    oldProperties, properties) : properties);
                }
            }
            e.remove();
        }
        System.out.println(doc);// now we have the result html without the
        // styles tags, and the inline css in each
        // element
    }

    private static String concatenateProperties(String oldProp, String newProp) {
        oldProp = oldProp.trim();
        if (!newProp.endsWith(";"))
           newProp += ";";
        return newProp + oldProp; // The existing (old) properties should take precedence.
    }
}


Using jsoup + cssparser:

private static final String STYLE_ATTR = "style";
private static final String CLASS_ATTR = "class";

public String inlineStyles(String html, File cssFile, boolean removeClasses) throws IOException {
    Document document = Jsoup.parse(html);
    CSSOMParser parser = new CSSOMParser(new SACParserCSS3());
    InputSource source = new InputSource(new FileReader(cssFile));
    CSSStyleSheet stylesheet = parser.parseStyleSheet(source, null, null);

    CSSRuleList ruleList = stylesheet.getCssRules();
    Map<Element, Map<String, String>> allElementsStyles = new HashMap<>();
    for (int ruleIndex = 0; ruleIndex < ruleList.getLength(); ruleIndex++) {
        CSSRule item = ruleList.item(ruleIndex);
        if (item instanceof CSSStyleRule) {
            CSSStyleRule styleRule = (CSSStyleRule) item;
            String cssSelector = styleRule.getSelectorText();
            Elements elements = document.select(cssSelector);
            for (Element element : elements) {
                Map<String, String> elementStyles = allElementsStyles.computeIfAbsent(element, k -> new LinkedHashMap<>());
                CSSStyleDeclaration style = styleRule.getStyle();
                for (int propertyIndex = 0; propertyIndex < style.getLength(); propertyIndex++) {
                    String propertyName = style.item(propertyIndex);
                    String propertyValue = style.getPropertyValue(propertyName);
                    elementStyles.put(propertyName, propertyValue);
                }
            }
        }
    }

    for (Map.Entry<Element, Map<String, String>> elementEntry : allElementsStyles.entrySet()) {
        Element element = elementEntry.getKey();
        StringBuilder builder = new StringBuilder();
        for (Map.Entry<String, String> styleEntry : elementEntry.getValue().entrySet()) {
            builder.append(styleEntry.getKey()).append(":").append(styleEntry.getValue()).append(";");
        }
        builder.append(element.attr(STYLE_ATTR));
        element.attr(STYLE_ATTR, builder.toString());
        if (removeClasses) {
            element.removeAttr(CLASS_ATTR);
        }
    }

    return document.html();
}


After hours of trying different manual java code solutions and not being satisfied with results (responsive media query handling issues mostly), I stumbled upon https://github.com/mdedetrich/java-premailer-wrapper which works great as a java solution. Note that you might actually be better off running your own "premailer" server. While there is a public api to premailer, I wanted to have my own instance running that I can hit as hard as I want: https://github.com/TrackIF/premailer-server

Easy to run on ec2 with just a few clicks: https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/create_deploy_Ruby_sinatra.html

git clone https://github.com/Enalmada/premailer-server
cd premailer-server
eb init  (choose latest ruby)
eb create premailer-server
eb deploy
curl --data "html=<your html>" http://your.eb.url


I haven't tried this but looks like you can use something like CSS parser to get a DOM tree corresponding to your CSS. So you can do something like:

  1. Obtain cssDOM
  2. Obtain htmlDOM (JAXP)
  3. Iterate over each cssDOM element and use xpath to locate and insert the correct style in your htmlDOM.
  4. Convert htmlDOM to string.


I can't yet comment but I wrote a gist that attempted to enhance the accepted answer to handle the cascading part of cascading stylesheets.

It doesn't work perfectly but it is almost there. https://gist.github.com/moodysalem/69e2966834a1f79492a9


You can use HtmlUnit and Jsoup. You render the html page in the browser using HtmlUnit. Then you get the computed styles going through the elements thanks to HtmlUnit. Jsoup is just here to format the html output.

You can find here a simple implementation :

public final class CssInliner {
   private static final Logger log = Logger.getLogger(CssInliner.class);

   private CssInliner() {
   }

   public static CssInliner make() {
      return new CssInliner();
   }

   /**
    * Main method
    *
    * @param html html to inline
    *
    * @return inlined html
    */
   public String inline(String html) throws IOException {

      try (WebClient webClient = new WebClient()) {

         HtmlPage htmlPage = getHtmlPage(webClient, html);
         Window window = webClient.getCurrentWindow().getScriptableObject();

         for (HtmlElement htmlElement : htmlPage.getHtmlElementDescendants()) {
            applyComputedStyle(window, htmlElement);
         }

         return outputCleanHtml(htmlPage);
      }
   }

   /**
    * Output the HtmlUnit page to a clean html. Remove the old global style tag
    * that we do not need anymore. This in order to simplify of the tests of the
    * output.
    *
    * @param htmlPage
    *
    * @return
    */
   private String outputCleanHtml(HtmlPage htmlPage) {
      Document doc = Jsoup.parse(htmlPage.getDocumentElement().asXml());
      Element globalStyleTag = doc.selectFirst("html style");
      if (globalStyleTag != null) {
         globalStyleTag.remove();
      }
      doc.outputSettings().syntax(Syntax.html);
      return doc.html();
   }

   /**
    * Modify the html elements by adding an style attribute to each element
    *
    * @param window
    * @param htmlElement
    */
   private void applyComputedStyle(Window window, HtmlElement htmlElement) {

      HTMLElement pj = htmlElement.getScriptableObject();
      ComputedCSSStyleDeclaration cssStyleDeclaration = window.getComputedStyle(pj, null);

      Map<String, StyleElement> map = getStringStyleElementMap(cssStyleDeclaration);
      // apply style element to html
      if (!map.isEmpty()) {
         htmlElement.writeStyleToElement(map);
      }
   }

   private Map<String, StyleElement> getStringStyleElementMap(ComputedCSSStyleDeclaration cssStyleDeclaration) {
      Map<String, StyleElement> map = new HashMap<>();
      for (Definition definition : Definition.values()) {
         String style = cssStyleDeclaration.getStyleAttribute(definition, false);

         if (StringUtils.isNotBlank(style)) {
            map.put(definition.getAttributeName(),
                    new StyleElement(definition.getAttributeName(),
                                     style,
                                     "",
                                     SelectorSpecificity.DEFAULT_STYLE_ATTRIBUTE));
         }

      }
      return map;
   }

   private HtmlPage getHtmlPage(WebClient webClient, String html) throws IOException {
      URL url = new URL("http://tinubuinliner/" + Math.random());
      StringWebResponse stringWebResponse = new StringWebResponse(html, url);

      return HTMLParser.parseHtml(stringWebResponse, webClient.getCurrentWindow());
   }
}


For a solution to this you're probably best using a battle hardened tool like the one from Mailchimp.

They've have opened up their css inlining tool in their API, see here: http://apidocs.mailchimp.com/api/1.3/inlinecss.func.php

Much more useful than a web form.

There's also an open source Ruby tool here: https://github.com/alexdunae/premailer/

Premailer also exposes an API and web form, see http://premailer.dialect.ca - it's sponsored by Campaign Monitor who are one of the other big players in the email space.

I'm guessing you could integrate Premailer into your Java app via [Jruby][1], although I have no experience with this.


The CSSBox + jStyleParser libraries can do the job as already answered here.


http://www.mailchimp.com/labs/inlinecss.php

Use that link above. It will save hours of your time and is made especially for email templates. It's a free tool by mailchimp


This kind of thing is often required for e-commerce applications where the bank/whatever doesn't allow linked CSS, e.g. WorldPay.

The big challenge isn't so much the conversion as the lack of inheritance. You have to explicitly set inherited properties on all descendant tags. Testing is vital as certain browsers will cause more grief than others. You will need to add a lot more inline code than you need for a linked stylesheet, for example in a linked stylesheet all you need is p { color:red }, but inline you have to explicitly set the color on all paragraphs.

From my experience, it's very much a manual process that requires a light touch and a lot of tweaking and cross-browser testing to get right.


I took the first two answers and adopted them to make use of the capabilities of the CSS parser library:

public String inline(String html, String styles) throws IOException {

    Document document = Jsoup.parse(html);

    CSSRuleList ruleList = getCssRules(styles);

    for (int i = 0; i < ruleList.getLength(); i++) {
        CSSRule rule = ruleList.item(i);
        if (rule instanceof CSSStyleRule) {
            CSSStyleRule styleRule = (CSSStyleRule) rule;
            String selector = styleRule.getSelectorText();

            Elements elements = document.select(selector);
            for (final Element element : elements) {
                applyRuleToElement(element, styleRule);
            }

        }

    }

    removeClasses(document);

    return document.html();
}

private CSSRuleList getCssRules(String styles) throws IOException {
    CSSOMParser parser = new CSSOMParser(new SACParserCSS3());
    CSSStyleSheet styleSheet = parser.parseStyleSheet(new InputSource(new StringReader(styles)), null, null);
    CSSRuleList list = styleSheet.getCssRules();
    return list;
}

private void applyRuleToElement(Element element, CSSStyleRule rule){
    String elementStyleString = element.attr("style");

    CSSStyleDeclarationImpl elementStyleDeclaration = new CSSStyleDeclarationImpl();
    elementStyleDeclaration.setCssText(elementStyleString);

    CSSStyleDeclarationImpl ruleStyleDeclaration = (CSSStyleDeclarationImpl)rule.getStyle();

    for(Property p : ruleStyleDeclaration.getProperties()){
        elementStyleDeclaration.addProperty(p);
    }

    String cssText = elementStyleDeclaration.getCssText();

    element.attr("style", cssText);
}

private void removeClasses(Document document){
    Elements elements = document.getElementsByAttribute("class");
    elements.removeAttr("class");
}

Maybe its possible to improve it further by using a CSS parser like https://github.com/phax/ph-css?

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜