【问题标题】:Automatically convert Style Sheets to inline style自动将样式表转换为内联样式
【发布时间】:2011-05-30 02:59:09
【问题描述】:

不必担心链接样式或悬停样式。

我想自动转换这样的文件

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

到这样的文件

<html>
<body style="background:red;font-weight:bold">
<p style="background:#FFC;font-weight:bold">...</p>
</body>
</html>

如果有一个 HTML 解析器可以做到这一点,我会更感兴趣。

我想这样做的原因是我可以显示使用全局样式表的电子邮件,而不会让它们的样式表弄乱我网页的其余部分。我还想将生成的样式发送到基于 Web 的富文本编辑器以进行回复和原始消息。

【问题讨论】:

  • What tools to automatically inline CSS style to create email HTML code ? 类似,但不是针对,也没有产生任何 Java 解决方案
  • 您正在构建一个网络邮件应用程序,对吧?如果有人发送width: expression(alert('hacked')); 之类的东西怎么办?你在报道吗?
  • 是的。转换后,我将拆分名称和值,然后要求两个名称值都匹配安全选项的正则表达式,并放回安全选项。
  • 抱歉,我们无法在电子邮件中保留样式表以符合 W3C 标准。您是否有指向您所指标准的链接? @varun

标签: java css html-parsing


【解决方案1】:

这是一个关于 java 的解决方案,我使用 JSoup 库完成的: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.
    }
}

【讨论】:

  • 我很确定这不符合 Java 解决方案的条件。
  • 这是一个很棒的解决方案,值得 +1。但需要注意的是,选择器继承和特异性规则是不被尊重的!
  • 请注意新的 CSS @media 规则打破了这个解决方案。
  • 此解决方案还会阻塞诸如 h{} 之类的并行标签,因为 { 和 } 将被 StringTokenizer 视为一个分隔符。
  • 注意:带有冒号的选择器会崩溃。 IE。 "a:link {...}" 将生成一个 org.jsoup.select.Selector$SelectorParseException
【解决方案2】:

使用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();
}

【讨论】:

  • 很好,但是它没有设置排序规则(例如按优先级),例如更具体的规则应该覆盖不太具体的规则
  • 附带说明,通过将 CSSOMParser parser = new CSSOMParser(); 更改为 CSSOMParser parser = new CSSOMParser(new SACParserCSS3()); 我能够更好地解析 CSS3。
  • Moody Salem 的回答不使用 CSSOMParser(因此 CSS 解析可能无法处理边缘情况),但看起来它确实尝试处理优先级。亲爱的lazyweb,请结合两个解决方案!
【解决方案3】:

在尝试不同的手动 java 代码解决方案并且对结果不满意(主要是响应式媒体查询处理问题)之后,我偶然发现了 https://github.com/mdedetrich/java-premailer-wrapper,它作为 java 解决方案非常有用。请注意,您实际上可能最好运行自己的“premailer”服务器。虽然 premailer 有一个公共 api,但我想让我自己的实例运行,我可以随心所欲地攻击: https://github.com/TrackIF/premailer-server

只需点击几下即可轻松在 ec2 上运行: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

【讨论】:

    【解决方案4】:

    我还没有尝试过,但看起来您可以使用 CSS parser 之类的东西来获取与您的 CSS 对应的 DOM 树。所以你可以这样做:

    1. 获取cssDOM
    2. 获取 htmlDOM (JAXP)
    3. 遍历每个 cssDOM 元素并使用 xpath 在您的 htmlDOM 中定位和插入正确的样式。
    4. 将 htmlDOM 转换为字符串。

    【讨论】:

      【解决方案5】:

      我还不能发表评论,但我写了一个要点,试图增强公认的答案以处理级联样式表的级联部分。

      它不能完美地工作,但它几乎就在那里。 https://gist.github.com/moodysalem/69e2966834a1f79492a9

      【讨论】:

        【解决方案6】:

        您可以使用HtmlUnitJsoup。您使用HtmlUnit 在浏览器中呈现html 页面。然后,感谢HtmlUnit,您可以通过元素获得计算出的样式。 Jsoup 只是用来格式化 html 输出。

        你可以在这里找到一个简单的实现:

        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());
           }
        }
        

        【讨论】:

          【解决方案7】:

          要解决这个问题,您最好使用像 Mailchimp 这样的久经沙场的工具。

          他们已经在他们的 API 中打开了他们的 css 内联工具,见这里:http://apidocs.mailchimp.com/api/1.3/inlinecss.func.php

          比网络表单更有用。

          这里还有一个开源的 Ruby 工具:https://github.com/alexdunae/premailer/

          Premailer 还公开了一个 API 和 Web 表单,请参阅 http://premailer.dialect.ca - 它由 Campaign Monitor 赞助,他们是电子邮件领域的其他大玩家之一。

          我猜你可以通过 [Jruby][1] 将 Premailer 集成到你的 Java 应用程序中,尽管我没有这方面的经验。

          【讨论】:

            【解决方案8】:

            CSSBox + jStyleParser 库可以完成已回答here 的工作。

            【讨论】:

              【解决方案9】:

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

              使用上面的链接。它将节省您数小时的时间,并且专为电子邮件模板制作。这是 mailchimp 的免费工具

              【讨论】:

              • 我必须能够在我们自己的服务器中使用代码。我们不能使用在线工具。
              • 你为什么不邮寄给他们并要求他们提供源代码?你可以安排商业销售或许可协议..
              • 好点...但我们不希望仅仅为了这件事而使用 PHP。 @Triztian
              【解决方案10】:

              银行/其他不允许链接 CSS 的电子商务应用程序通常需要这种东西,例如WorldPay。

              最大的挑战与其说是转换,不如说是缺乏继承。您必须在所有后代标签上显式设置继承属性。测试至关重要,因为某些浏览器会比其他浏览器造成更多的痛苦。您需要添加比链接样式表更多的内联代码,例如在链接样式表中,您只需要p { color:red },但内联您必须明确设置所有段落的颜色。

              根据我的经验,这是一个非常手动的过程,需要轻轻一点、大量调整和跨浏览器测试才能正确。

              【讨论】:

              • 这不能是手动过程。它必须在服务器端是自动的。
              【解决方案11】:

              我采用了前两个答案并采用它们来利用 CSS 解析器库的功能:

              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");
              }
              

              也许可以通过使用像https://github.com/phax/ph-css这样的CSS解析器来进一步改进它?

              【讨论】:

                猜你喜欢
                • 2013-06-26
                • 2014-09-25
                • 2014-12-07
                • 1970-01-01
                • 1970-01-01
                • 2021-08-23
                • 1970-01-01
                • 1970-01-01
                • 2021-01-14
                相关资源
                最近更新 更多