Skip to content

Commit

Permalink
Added style sheet parser
Browse files Browse the repository at this point in the history
  • Loading branch information
M66B committed Apr 20, 2020
1 parent d464d85 commit 2deb6e1
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 4 deletions.
2 changes: 2 additions & 0 deletions ATTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ FairEmail uses:
* [AppAuth for Android](https://github.com/openid/AppAuth-Android). Copyright 2015 The AppAuth for Android Authors. All Rights Reserved. [Apache License 2.0](https://github.com/openid/AppAuth-Android/blob/master/LICENSE).
* [JCharset](http://www.freeutils.net/source/jcharset/). Copyright (C) 1989, 1991 Free Software Foundation, Inc. [GNU General Public License](http://www.freeutils.net/source/jcharset/#license).
* [Material design icons](https://github.com/google/material-design-icons). Copyright ???. [Apache license version 2.0](https://github.com/google/material-design-icons#user-content-license).
* [CSS Parser](http://cssparser.sourceforge.net/). Copyright © 1999–2019. All rights reserved. [Apache License, Version 2.0](http://cssparser.sourceforge.net/licenses.html).
* [Java™ Architecture for XML Binding](https://github.com/eclipse-ee4j/jaxb-ri). Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. [GNU General Public License Version 2](https://github.com/eclipse-ee4j/jaxb-ri/blob/master/jaxb-ri/LICENSE.md).
10 changes: 10 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ dependencies {
def billingclient_version = "2.2.0"
def javamail_version = "1.6.5"
def jsoup_version = "1.13.1"
def css_version = "0.9.27"
def jax_version = "2.3.0-jaxb-1.0.6"
def dnsjava_version = "2.1.9"
def openpgp_version = "12.0"
def requery_version = "3.31.0"
Expand Down Expand Up @@ -341,6 +343,14 @@ dependencies {
// https://jsoup.org/news/
implementation "org.jsoup:jsoup:$jsoup_version"

// http://cssparser.sourceforge.net/
// https://mvnrepository.com/artifact/net.sourceforge.cssparser/cssparser
implementation "net.sourceforge.cssparser:cssparser:$css_version"

// https://github.com/eclipse-ee4j/jaxb-ri
// https://mvnrepository.com/artifact/org.w3c/dom
implementation "org.w3c:dom:$jax_version"

// http://www.dnsjava.org/
// https://mvnrepository.com/artifact/dnsjava/dnsjava
implementation "dnsjava:dnsjava:$dnsjava_version"
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/assets/ATTRIBUTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ FairEmail uses:
* [AppAuth for Android](https://github.com/openid/AppAuth-Android). Copyright 2015 The AppAuth for Android Authors. All Rights Reserved. [Apache License 2.0](https://github.com/openid/AppAuth-Android/blob/master/LICENSE).
* [JCharset](http://www.freeutils.net/source/jcharset/). Copyright (C) 1989, 1991 Free Software Foundation, Inc. [GNU General Public License](http://www.freeutils.net/source/jcharset/#license).
* [Material design icons](https://github.com/google/material-design-icons). Copyright ???. [Apache license version 2.0](https://github.com/google/material-design-icons#user-content-license).
* [CSS Parser](http://cssparser.sourceforge.net/). Copyright © 1999–2019. All rights reserved. [Apache License, Version 2.0](http://cssparser.sourceforge.net/licenses.html).
* [Java™ Architecture for XML Binding](https://github.com/eclipse-ee4j/jaxb-ri). Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. [GNU General Public License Version 2](https://github.com/eclipse-ee4j/jaxb-ri/blob/master/jaxb-ri/LICENSE.md).
109 changes: 105 additions & 4 deletions app/src/main/java/eu/faircode/email/HtmlHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@
import androidx.core.util.PatternsCompat;
import androidx.preference.PreferenceManager;

import com.steadystate.css.dom.CSSStyleRuleImpl;
import com.steadystate.css.parser.CSSOMParser;
import com.steadystate.css.parser.SACParserCSS3;
import com.steadystate.css.parser.selectors.ClassConditionImpl;
import com.steadystate.css.parser.selectors.ConditionalSelectorImpl;

import org.jsoup.nodes.Attribute;
import org.jsoup.nodes.Comment;
import org.jsoup.nodes.Document;
Expand All @@ -56,12 +62,21 @@
import org.jsoup.select.NodeFilter;
import org.jsoup.select.NodeTraversor;
import org.jsoup.select.NodeVisitor;
import org.w3c.css.sac.CSSException;
import org.w3c.css.sac.CSSParseException;
import org.w3c.css.sac.ErrorHandler;
import org.w3c.css.sac.InputSource;
import org.w3c.css.sac.Selector;
import org.w3c.dom.css.CSSRule;
import org.w3c.dom.css.CSSRuleList;
import org.w3c.dom.css.CSSStyleSheet;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand All @@ -74,6 +89,7 @@

import static androidx.core.text.HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM;
import static androidx.core.text.HtmlCompat.TO_HTML_PARAGRAPH_LINES_CONSECUTIVE;
import static org.w3c.css.sac.Condition.SAC_CLASS_CONDITION;

public class HtmlHelper {
private static final int PREVIEW_SIZE = 500; // characters
Expand Down Expand Up @@ -356,8 +372,42 @@ public FilterResult tail(Node node, int depth) {
.text(context.getString(R.string.title_show_full));
}

// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/style
CSSStyleSheet sheet = null;
for (Element style : parsed.head().select("style")) {
Log.i("Style=" + style.data());
try {
InputSource source = new InputSource(new StringReader(style.data()));
CSSOMParser parser = new CSSOMParser(new SACParserCSS3());
parser.setErrorHandler(new ErrorHandler() {
@Override
public void warning(CSSParseException ex) throws CSSException {
Log.w(ex);
}

@Override
public void error(CSSParseException ex) throws CSSException {
Log.e(ex);
}

@Override
public void fatalError(CSSParseException ex) throws CSSException {
Log.e(ex);
}
});

// TODO: media queries
CSSStyleSheet s = parser.parseStyleSheet(source, null, null);
if (s.getMedia() != null && "all".equals(s.getMedia().getMediaText()))
sheet = s;
} catch (Throwable ex) {
Log.w(ex);
}
}

Whitelist whitelist = Whitelist.relaxed()
.addTags("hr", "abbr", "big", "font", "dfn", "del", "s", "tt")
.addAttributes(":all", "class")
.addAttributes(":all", "style")
.addAttributes("font", "size")
.removeTags("col", "colgroup", "thead", "tbody")
Expand Down Expand Up @@ -412,16 +462,46 @@ else if (s > 3)

// Sanitize styles
for (Element element : document.select("*")) {
String clazz = element.attr("class");
String style = element.attr("style");

// Process class
if (!TextUtils.isEmpty(clazz) && sheet != null) {
CSSRuleList rules = sheet.getCssRules();
for (int i = 0; rules != null && i < rules.getLength(); i++) {
CSSRule rule = rules.item(i);
if (rule.getType() == CSSRule.STYLE_RULE) {
CSSStyleRuleImpl srule = (CSSStyleRuleImpl) rule;
for (int j = 0; j < srule.getSelectors().getLength(); j++) {
Selector selector = srule.getSelectors().item(j);
switch (selector.getSelectorType()) {
case Selector.SAC_ANY_NODE_SELECTOR:
style = mergeStyles(srule.getStyle().getCssText(), style);
break;
case Selector.SAC_CONDITIONAL_SELECTOR:
ConditionalSelectorImpl cselector = (ConditionalSelectorImpl) selector;
if (cselector.getCondition().getConditionType() == SAC_CLASS_CONDITION) {
ClassConditionImpl ccondition = (ClassConditionImpl) cselector.getCondition();
if (clazz.equals(ccondition.getValue()))
style = mergeStyles(srule.getStyle().getCssText(), style);
}
break;
}
}
}
}
}

// Process style
if (!TextUtils.isEmpty(style)) {
StringBuilder sb = new StringBuilder();

String[] params = style.split(";");
for (String param : params) {
int semi = param.indexOf(':');
if (semi > 0) {
String key = param.substring(0, semi).trim().toLowerCase(Locale.ROOT);
String value = param.substring(semi + 1).toLowerCase(Locale.ROOT)
int colon = param.indexOf(':');
if (colon > 0) {
String key = param.substring(0, colon).trim().toLowerCase(Locale.ROOT);
String value = param.substring(colon + 1).toLowerCase(Locale.ROOT)
.replace("!important", "")
.trim()
.replaceAll("\\s+", " ");
Expand Down Expand Up @@ -845,6 +925,27 @@ public void tail(Node node, int depth) {
return document;
}

private static String mergeStyles(String base, String style) {
Map<String, String> result = new HashMap<>();

List<String> params = new ArrayList<>();
if (!TextUtils.isEmpty(base))
params.addAll(Arrays.asList(base.split(";")));
if (!TextUtils.isEmpty(style))
params.addAll(Arrays.asList(style.split(";")));

for (String param : params) {
int colon = param.indexOf(':');
if (colon > 0) {
String key = param.substring(0, colon).trim().toLowerCase(Locale.ROOT);
result.put(key, param);
} else
Log.w("Invalid style param=" + param);
}

return TextUtils.join(";", result.values());
}

private static Integer getFontWeight(String value) {
if (TextUtils.isEmpty(value))
return null;
Expand Down

0 comments on commit 2deb6e1

Please sign in to comment.