web-dev-qa-db-fra.com

Convertir automatiquement les feuilles de style en style intégré

Ne vous inquiétez pas du style lié ou du style en vol stationnaire.

Je veux convertir automatiquement des fichiers comme celui-ci

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

à des fichiers comme celui-ci

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

Je serais encore plus intéressé s'il y avait un analyseur HTML qui le ferait.

La raison pour laquelle je veux faire cela est que je peux afficher des courriels qui utilisent des feuilles de style globales sans que celles-ci ne gâchent le reste de ma page Web. Je souhaite également envoyer le style résultant à un éditeur de texte enrichi basé sur le Web pour obtenir une réponse et un message original.

35
George Bailey

Voici une solution sur Java, je l’ai faite avec la bibliothèque 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.
    }
}
29
Grekz

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

Après des heures à essayer différentes solutions de code Java manuel et à ne pas être satisfait des résultats (problèmes de gestion des requêtes de médias sensibles, principalement), je suis tombé sur https://github.com/mdedetrich/Java-premailer-wrapper une solution Java. Notez que vous pouvez en fait mieux utiliser votre propre serveur "premailer". Bien qu'il y ait une api publique pour premailer, je voulais avoir ma propre instance en marche que je peux frapper aussi fort que je veux: https://github.com/TrackIF/premailer-server

Facile à exécuter sur ec2 en quelques clics: 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
3
Adam Lane

Je ne peux pas encore commenter, mais j'ai écrit un Gist qui tentait d'améliorer la réponse acceptée pour gérer la partie en cascade des feuilles de style en cascade.

Cela ne fonctionne pas parfaitement mais il est presque là. https://Gist.github.com/moodysalem/69e2966834a1f79492a9

2
Moody Salem

Vous pouvez utiliser HtmlUnit et Jsoup. Vous rendez la page html dans le navigateur en utilisant HtmlUnit. Ensuite, vous obtenez les styles calculés en passant par les éléments grâce à HtmlUnit. Jsoup est juste ici pour formater la sortie HTML.

Vous pouvez trouver ici une implémentation simple:

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
Fabrice

Je n'ai pas essayé cela, mais il semble que vous puissiez utiliser quelque chose comme analyseur CSS pour obtenir un arbre DOM correspondant à votre CSS. Donc, vous pouvez faire quelque chose comme:

  1. Obtenir le cssDOM
  2. Obtenir htmlDOM (JAXP)
  3. Parcourez chaque élément cssDOM et utilisez xpath pour localiser et insérer le style correct dans votre htmlDOM.
  4. Convertissez htmlDOM en chaîne.
2
Abdullah Jibaly

Pour une solution à ce problème, il vaut probablement mieux utiliser un outil de combat dur comme celui de Mailchimp.

Ils ont ouvert leur outil d’inclusion css dans leur API, voir ici: http://apidocs.mailchimp.com/api/1.3/inlinecss.func.php

Beaucoup plus utile qu'un formulaire Web.

Il existe également un outil open source Ruby ici: https://github.com/alexdunae/premailer/

Premailer expose également une API et un formulaire Web, voir http://premailer.dialect.ca - il est sponsorisé par Campaign Monitor, qui est l’un des autres gros joueurs de l’espace courriel.

Je suppose que vous pourriez intégrer Premailer dans votre application Java via [Jruby] [1], bien que je n’aie aucune expérience en la matière.

1
Jed Watson

Les bibliothèques CSSBox + jStyleParser peuvent faire le travail comme déjà répondu here .

0
radkovo

Ce genre de chose est souvent nécessaire pour les applications de commerce électronique où la banque/ce qui ne permet pas le CSS lié, par exemple. WorldPay.

Le grand défi n'est pas tant la conversion que le manque d'héritage. Vous devez définir explicitement les propriétés héritées sur toutes les balises descendantes. Les tests sont vitaux car certains navigateurs causeront plus de chagrin que d'autres. Vous aurez besoin d'ajouter beaucoup plus de code en ligne que nécessaire pour une feuille de style liée, par exemple, dans une feuille de style liée, tout ce dont vous avez besoin est p { color:red }, mais vous devez définir la couleur de manière explicite sur tous les paragraphes.

D'après mon expérience, il s'agit en réalité d'un processus manuel qui nécessite une manipulation légère et de nombreux ajustements et tests sur plusieurs navigateurs pour être efficace.

0
GlennG

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

Utilisez ce lien ci-dessus. Cela vous fera gagner des heures et est spécialement conçu pour les modèles de courrier électronique. C'est un outil gratuit par mailchimp 

0
owilde1900

J'ai pris les deux premières réponses et les ai adoptées pour utiliser les fonctionnalités de la bibliothèque d'analyseur 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");
}

Peut-être est-il possible de l'améliorer davantage en utilisant un analyseur CSS tel que https://github.com/phax/ph-css ?

0
Mathias