Last active
November 23, 2023 08:12
-
-
Save moodysalem/69e2966834a1f79492a9 to your computer and use it in GitHub Desktop.
Attempt to inline CSS using Java HTML parsing library jsoup
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 any chance of putting this on maven central via Sonatype?
@moodysalem have you thought about using new CSSOMParser(new SACParserCSS3())
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)']]
I would strongly recommend you use premailer instead of doing this yourself in Java.
@moodysalem Thanks for reply. Premail looking little bit outdated, I found this: https://github.com/Automattic/juice
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
An attempt to enhance the example provided here to properly handle the cascading part of cascading stylesheets