Автоматическое преобразование таблиц стилей во встроенный стиль - PullRequest
36 голосов
/ 23 декабря 2010

Не нужно беспокоиться о связанном стиле или стиле наведения.

Я хочу автоматически преобразовывать файлы, подобные этому

<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-парсер, который бы делал это.

Причина, по которой я хочу это сделать, заключается в том, чтобы я мог отображать электронные письма, использующие глобальные таблицы стилей, без их таблиц стилейостальная часть моей веб-страницы.Я также хотел бы отправить полученный стиль в веб-редактор форматированного текста для ответа и исходного сообщения.

Ответы [ 11 ]

29 голосов
/ 23 декабря 2010

Вот решение для 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.
    }
}
9 голосов
/ 13 февраля 2014

Использование 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();
}
3 голосов
/ 19 августа 2016

После нескольких часов попыток использования различных ручных решений для Java-кода и неудовлетворенности результатами (в основном это были проблемы с реагированием на медийные запросы), я наткнулся на https://github.com/mdedetrich/java-premailer-wrapper, который прекрасно работает в качестве решения Java.Обратите внимание, что на самом деле вам лучше использовать собственный сервер «premailer».Несмотря на то, что есть общедоступный API для premailer, я хотел, чтобы у меня был запущен собственный экземпляр, который я могу нажимать так сильно, как хочу: 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
2 голосов
/ 24 мая 2018

Вы можете использовать HtmlUnit и Jsoup.Вы отображаете HTML-страницу в браузере, используя HtmlUnit.Затем вы получаете вычисленные стили, проходящие через элементы благодаря 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());
   }
}
2 голосов
/ 18 октября 2015

Я пока не могу комментировать, но я написал суть, которая пыталась улучшить принятый ответ для обработки каскадной части каскадных таблиц стилей.

Это не работает идеально, но это почти там. https://gist.github.com/moodysalem/69e2966834a1f79492a9

2 голосов
/ 24 декабря 2010

Я не пробовал этого, но похоже, что вы можете использовать что-то вроде CSS-парсер , чтобы получить дерево DOM, соответствующее вашему CSS.Таким образом, вы можете сделать что-то вроде:

  1. Получить cssDOM
  2. Получить htmlDOM (JAXP)
  3. Итерировать по каждому элементу cssDOM и использовать xpath, чтобы найти и вставить правильный стильв вашем htmlDOM.
  4. Преобразование htmlDOM в строку.
1 голос
/ 02 января 2019

Библиотеки CSSBox + jStyleParser могут выполнить работу, как уже отвечено здесь .

1 голос
/ 22 октября 2012

Для решения этой проблемы вам, вероятно, лучше всего использовать боевой инструмент, такой как от Mailchimp.

Они открыли свой инструмент css inlining в своем API, см. Здесь: http://apidocs.mailchimp.com/api/1.3/inlinecss.func.php

Гораздо полезнее, чем веб-форма.

Здесь также есть инструмент Ruby с открытым исходным кодом: https://github.com/alexdunae/premailer/

Premailer также предоставляет API и веб-форму, см. http://premailer.dialect.ca - его спонсором является Campaign Monitor, который является одним из других крупных игроков в области электронной почты.

Я предполагаю, что вы можете интегрировать Premailer в свое Java-приложение через [Jruby] [1], хотяУ меня нет опыта с этим.

0 голосов
/ 17 мая 2018

Я взял первые два ответа и принял их, чтобы использовать возможности библиотеки синтаксического анализатора 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");
}

Возможно, можно улучшить его, используя синтаксический анализатор CSS, например https://github.com/phax/ph-css?

0 голосов
/ 25 декабря 2010

Подобные вещи часто требуются для приложений электронной коммерции, где банк / что-либо еще не разрешает связанный CSS, например, WorldPay.

Большой проблемой является не столько конверсия, сколько отсутствиенаследование.Вы должны явно установить унаследованные свойства для всех тегов-потомков.Тестирование жизненно важно, поскольку некоторые браузеры вызывают больше горя, чем другие.Вам нужно будет добавить гораздо больше встроенного кода, чем нужно для связанной таблицы стилей, например, в связанной таблице стилей все, что вам нужно, это p { color:red }, но для встроенной необходимо явно установить цвет для всех абзацев.

Исходя из моего опыта, это очень ручной процесс, требующий легкого прикосновения, большого количества настроек и кросс-браузерного тестирования, чтобы получить правильные результаты.

...