Skip to content

Instantly share code, notes, and snippets.

@moodysalem
Last active November 23, 2023 08:12
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save moodysalem/69e2966834a1f79492a9 to your computer and use it in GitHub Desktop.
Save moodysalem/69e2966834a1f79492a9 to your computer and use it in GitHub Desktop.
Attempt to inline CSS using Java HTML parsing library jsoup
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;
public class CSSInliner {
private static final String STYLE = "style";
private static final String DELIMS = "{}";
private static final Logger LOG = Logger.getLogger(CSSInliner.class.getName());
private static final Priority STYLE_PRIORITY = new Priority(1, 0, 0, 0);
public static String process(String html) {
long t1 = System.nanoTime();
// process the html doc
Document doc = Jsoup.parse(html);
// get all the style tags
Elements styleTags = doc.select(STYLE);
// this map stores all the styles we need to apply to the elements
HashMap<String, HashMap<String, ValueWithPriority>> stylesToApply = new HashMap<>();
for (Element style : styleTags) {
String rules = style.getAllElements().get(0).data()
.replaceAll("\n", "") // remove newlines
.replaceAll("\\/\\*[^*]*\\*+([^/*][^*]*\\*+)*\\/", "") // remove comments
.trim();
StringTokenizer st = new StringTokenizer(rules, DELIMS);
while (st.countTokens() > 1) {
String selector = st.nextToken().trim();
// the list of css styles for the selector
String properties = st.nextToken().trim();
String[] splitSelectors = selector.split(",");
for (String sel : splitSelectors) {
String trimmedSel = sel.trim();
Elements selectedElements = doc.select(trimmedSel);
for (Element selElem : selectedElements) {
if (selElem.equals(style)) {
LOG.log(Level.SEVERE, "Style tag selected by " + trimmedSel);
continue;
}
HashMap<String, ValueWithPriority> existingStyles;
String exactSel = selElem.cssSelector();
if (!stylesToApply.containsKey(exactSel)) {
existingStyles = stylesOf(STYLE_PRIORITY, selElem.attr(STYLE));
} else {
existingStyles = stylesToApply.get(exactSel);
}
stylesToApply.put(exactSel,
mergeStyle(
existingStyles,
stylesOf(getPriority(trimmedSel), properties)
)
);
}
}
}
style.remove();
}
// apply the styles
for (String exactSelector : stylesToApply.keySet()) {
Element toApply = doc.select(exactSelector).first();
if (toApply == null) {
LOG.log(Level.SEVERE, "Failed to find " + exactSelector);
continue;
}
HashMap<String, ValueWithPriority> styles = stylesToApply.get(exactSelector);
StringBuilder sb = new StringBuilder();
for (String property : styles.keySet()) {
sb.append(property).append(":").append(styles.get(property).getValue()).append(";");
}
toApply.attr(STYLE, sb.toString());
}
long t2 = System.nanoTime();
LOG.log(Level.INFO, "Spent " + ((t2 - t1) / 1000L) + " milliseconds inlining CSS");
return doc.toString();
}
private static class Priority implements Comparable<Priority> {
private int a;
private int b;
private int c;
private int d;
public Priority(int a, int b, int c, int d) {
this.a = a;
this.b = b;
this.c = c;
this.d = d;
}
public int getA() {
return a;
}
public void setA(int a) {
this.a = a;
}
public int getB() {
return b;
}
public void setB(int b) {
this.b = b;
}
public int getC() {
return c;
}
public void setC(int c) {
this.c = c;
}
public int getD() {
return d;
}
public void setD(int d) {
this.d = d;
}
@Override
public boolean equals(Object obj) {
if (obj == null || !(obj instanceof Priority)) {
return false;
}
Priority other = (Priority) obj;
return other.getA() == getA() && other.getB() == getB() && other.getC() == getC() && other.getD() == getD();
}
@Override
public int compareTo(Priority o) {
if (o == this) {
return 0;
}
int comp = Integer.compare(getA(), o.getA());
if (comp != 0) {
return comp;
}
comp = Integer.compare(getB(), o.getB());
if (comp != 0) {
return comp;
}
comp = Integer.compare(getC(), o.getC());
if (comp != 0) {
return comp;
}
return Integer.compare(getD(), o.getD());
}
}
private static class ValueWithPriority {
private String value;
private Priority priority;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public Priority getPriority() {
return priority;
}
public void setPriority(Priority priority) {
this.priority = priority;
}
}
private static final HashMap<String, Priority> PRIORITY_CACHE = new HashMap<>();
private static Priority getPriority(String selector) {
selector = selector.trim();
if (PRIORITY_CACHE.containsKey(selector)) {
return PRIORITY_CACHE.get(selector);
}
int b = 0, c = 0, d = 0;
String[] pieces = selector.split(" ");
for (String pc : pieces) {
if (pc == null || pc.trim().length() == 0) {
continue;
}
pc = pc.trim();
if (pc.startsWith("#")) {
b++;
continue;
}
if (pc.contains("[") || pc.startsWith(".") || (pc.contains(":") && (!pc.contains("::")))) {
c++;
continue;
}
d++;
}
Priority p = new Priority(0, b, c, d);
PRIORITY_CACHE.put(selector, p);
return p;
}
private static HashMap<String, ValueWithPriority> stylesOf(Priority priority, String properties) {
HashMap<String, ValueWithPriority> vp = new HashMap<>();
if (properties == null || properties.trim().length() == 0) {
return vp;
}
String[] props = properties.split(";");
if (props.length > 1) {
for (String p : props) {
String[] pcs = p.split(":");
if (pcs.length != 2) {
continue;
}
String name = pcs[0].trim(), value = pcs[1].trim();
ValueWithPriority vwp = new ValueWithPriority();
vwp.setPriority(priority);
vwp.setValue(value);
vp.put(name, vwp);
}
}
return vp;
}
private static HashMap<String, ValueWithPriority> mergeStyle(HashMap<String, ValueWithPriority> oldProps,
HashMap<String, ValueWithPriority> newProps) {
HashMap<String, ValueWithPriority> finalProps = new HashMap<>();
Set<String> allProps = new HashSet<>(oldProps.keySet());
allProps.addAll(newProps.keySet());
for (String p : allProps) {
ValueWithPriority oldValue = oldProps.get(p);
ValueWithPriority newValue = newProps.get(p);
if (oldValue == null && newValue == null) {
continue;
}
if (oldValue == null) {
finalProps.put(p, newValue);
continue;
}
if (newValue == null) {
finalProps.put(p, oldValue);
continue;
}
int compare = oldValue.getPriority().compareTo(newValue.getPriority());
if (compare < 0) {
finalProps.put(p, newValue);
} else {
finalProps.put(p, oldValue);
}
}
return finalProps;
}
}
@moodysalem
Copy link
Author

An attempt to enhance the example provided here to properly handle the cascading part of cascading stylesheets

@graingert
Copy link

graingert commented Jul 3, 2017

@moodysalem any chance of putting this on maven central via Sonatype?

@graingert
Copy link

@moodysalem have you thought about using new CSSOMParser(new SACParserCSS3())

@landsman
Copy link

How to handle with media queries? It would stay in HTML with class name I think

Could not parse query '@media only screen and (max-width: 640px)': unexpected token at '@media only screen and (max-width: 640px)']]

@moodysalem
Copy link
Author

I would strongly recommend you use premailer instead of doing this yourself in Java.

https://pypi.org/project/premailer/

@landsman
Copy link

@moodysalem Thanks for reply. Premail looking little bit outdated, I found this: https://github.com/Automattic/juice

@cthornton
Copy link

FYI there's a bug with this line:

if (props.length > 1) {

This prevents loading any CSS rules with only one rule. I deleted that if statement, and it seems to be working for me

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment