#1585 SVG import - support for style tag (CSS)

This commit is contained in:
Jindra Petřík
2021-03-11 15:44:53 +01:00
parent 5158046b06
commit 4832ca7bc8
10 changed files with 3766 additions and 13 deletions

View File

@@ -12,7 +12,8 @@
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library.
* License along with this library.
*/
package com.jpexs.decompiler.flash.importers.svg;
import com.jpexs.decompiler.flash.ReadOnlyTagList;
@@ -24,6 +25,9 @@ import com.jpexs.decompiler.flash.exporters.modes.ShapeExportMode;
import com.jpexs.decompiler.flash.exporters.settings.ShapeExportSettings;
import com.jpexs.decompiler.flash.exporters.shape.BitmapExporter;
import com.jpexs.decompiler.flash.importers.ShapeImporter;
import com.jpexs.decompiler.flash.importers.svg.css.CssParseException;
import com.jpexs.decompiler.flash.importers.svg.css.CssParser;
import com.jpexs.decompiler.flash.importers.svg.css.CssSelectorToXPath;
import com.jpexs.decompiler.flash.tags.DefineShape4Tag;
import com.jpexs.decompiler.flash.tags.ExportAssetsTag;
import com.jpexs.decompiler.flash.tags.Tag;
@@ -66,8 +70,13 @@ import javax.imageio.ImageIO;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
@@ -211,6 +220,55 @@ public class SvgImporter {
}
}
private void processStyle(Element element) {
String styleSheet = element.getTextContent().trim();
CssParser cssParser = new CssParser(styleSheet);
CssSelectorToXPath selectorToXPath = new CssSelectorToXPath();
Document doc = element.getOwnerDocument();
try {
cssParser.styleshet();
XPath xPath = XPathFactory.newInstance().newXPath();
for (int i = 0; i < cssParser.getCountRulesets(); i++) {
String selector = cssParser.getSelector(i);
String xPathExpression = selectorToXPath.css2xpath(selector);
try {
NodeList nodeList = (NodeList) xPath.compile(xPathExpression).evaluate(doc, XPathConstants.NODESET);
for (int j = 0; j < nodeList.getLength(); j++) {
Node node = nodeList.item(j);
NamedNodeMap attrs = node.getAttributes();
Node styleAttr = attrs.getNamedItem("ffdec-style");
if (styleAttr != null) {
styleAttr.setNodeValue(styleAttr.getNodeValue() + ";" + "{" + cssParser.getSpecifity(i) + "}" + cssParser.getDeclarations(i));
attrs.setNamedItem(styleAttr);
} else {
Node styleNode = doc.createAttribute("ffdec-style");
styleNode.setNodeValue("{" + cssParser.getSpecifity(i) + "}" + cssParser.getDeclarations(i));
attrs.setNamedItem(styleNode);
}
}
} catch (XPathExpressionException ex) {
Logger.getLogger(SvgImporter.class.getName()).log(Level.SEVERE, null, ex);
}
}
} catch (IOException | CssParseException ex) {
showWarning("CannotParseCSSStyle", "Cannot parse CSS style: " + ex.getMessage());
}
}
private void processDefs(Element element) {
for (int i = 0; i < element.getChildNodes().getLength(); i++) {
Node childNode = element.getChildNodes().item(i);
if (childNode instanceof Element) {
Element childElement = (Element) childNode;
String tagName = childElement.getTagName();
if ("style".equals(tagName)) {
processStyle(childElement);
} else if ("defs".equals(tagName)) {
processDefs(childElement);
}
}
}
}
private void processSvgObject(Map<String, Element> idMap, int shapeNum, SHAPEWITHSTYLE shapes, Element element, Matrix transform, SvgStyle style) {
for (int i = 0; i < element.getChildNodes().getLength(); i++) {
Node childNode = element.getChildNodes().item(i);
@@ -220,7 +278,9 @@ public class SvgImporter {
SvgStyle newStyle = new SvgStyle(this, idMap, childElement);
Matrix m = Matrix.parseSvgMatrix(childElement.getAttribute("transform"), 1, 1);
Matrix m2 = m == null ? transform : transform.concatenate(m);
Matrix m2 = m == null ? transform : transform.concatenate(m);
if ("style".equals(tagName)) {
processStyle(childElement);
} else if ("g".equals(tagName)) {
processSvgObject(idMap, shapeNum, shapes, childElement, m2, newStyle);
} else if ("path".equals(tagName)) {
processPath(shapeNum, shapes, childElement, m2, newStyle);
@@ -236,7 +296,9 @@ public class SvgImporter {
processPolyline(shapeNum, shapes, childElement, m2, newStyle);
} else if ("polygon".equals(tagName)) {
processPolygon(shapeNum, shapes, childElement, m2, newStyle);
processPolygon(shapeNum, shapes, childElement, m2, newStyle);
} else if ("defs".equals(tagName)) {
processDefs(childElement);
} else if ("title".equals(tagName) || "desc".equals(tagName)
|| "radialGradient".equals(tagName) || "linearGradient".equals(tagName)) {
// ignore
} else {
@@ -1035,6 +1097,7 @@ public class SvgImporter {
//Stub for w3 test. TODO: refactor and move to test directory. It's here because of easy access - compiling single file
private static void svgTest(String name) throws IOException, InterruptedException {
System.err.println("running test " + name);
if (!new File(name + ".original.svg").exists()) {
URL svgUrl = new URL("http://www.w3.org/Graphics/SVG/Test/20061213/svggen/" + name + ".svg");
byte[] svgData = Helper.readStream(svgUrl.openStream());
@@ -1048,7 +1111,7 @@ public class SvgImporter {
String svgDataS = Helper.readTextFile(name + ".orig.svg");
SWF swf = new SWF();
DefineShape4Tag st = new DefineShape4Tag(swf);
DefineShape4Tag st = new DefineShape4Tag(swf);
st = (DefineShape4Tag) (new SvgImporter().importSvg(st, svgDataS, false));
swf.addTag(st);
SerializableImage si = new SerializableImage(480, 360, BufferedImage.TYPE_4BYTE_ABGR);
BitmapExporter.export(swf, st.shapes, Color.yellow, si, new Matrix(), new Matrix(), null);
@@ -1301,13 +1364,13 @@ public class SvgImporter {
// svgTest("struct-use-01-t");
// svgTest("struct-use-03-t");
// svgTest("struct-use-05-b");
// svgTest("struct-use-05-b");
// svgTest("styling-css-01-b");
// svgTest("styling-css-02-b");
// svgTest("styling-css-03-b");
svgTest("styling-css-01-b");
svgTest("styling-css-02-b");
svgTest("styling-css-03-b");
svgTest("styling-css-04-f");
// svgTest("styling-css-05-b");
// svgTest("styling-css-06-b");
// svgTest("styling-css-06-b");
svgTest("styling-inherit-01-b");
// svgTest("styling-pres-01-t");
// svgTest("text-align-01-b");
// svgTest("text-align-02-b");
@@ -1341,7 +1404,7 @@ public class SvgImporter {
// svgTest("text-tspan-01-b");
// svgTest("text-ws-01-t");
// svgTest("text-ws-02-t");
// svgTest("text-ws-02-t");
// svgTest("types-basicDOM-01-b");
}
private void applyFillGradients(SvgFill fill, FILLSTYLE fillStyle, RECT bounds, StyleChangeRecord scr, Matrix transform, int shapeNum, SvgStyle style) {

View File

@@ -12,7 +12,8 @@
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library.
* License along with this library.
*/
package com.jpexs.decompiler.flash.importers.svg;
import com.jpexs.decompiler.flash.importers.ShapeImporter;
@@ -21,6 +22,8 @@ import com.jpexs.helpers.Helper;
import java.awt.Color;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -56,9 +59,42 @@ class SvgStyle {
private Map<String, String> getStyleAttributeValues(Element element) {
// todo: cache
Map<String, String> styleValues = new HashMap<>();
String styleStr = "";
if (element.hasAttribute("ffdec-style")) {
styleStr += element.getAttribute("ffdec-style");
}
if (element.hasAttribute("style")) {
if (element.hasAttribute("style")) {
styleStr += "{1000}" + element.getAttribute("style");
}
if (!styleStr.isEmpty()) {
String[] rulesBySpec = styleStr.split("\\{");
List<String> rulesBySpecList = Arrays.asList(rulesBySpec);
List<String> rulesBySpecListUnordered = new ArrayList<>(rulesBySpecList);
rulesBySpecList.sort(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
if (o1.isEmpty()) {
return -1;
}
if (o2.isEmpty()) {
return 1;
}
int r1 = Integer.parseInt(o1.substring(0, o1.indexOf("}")));
int r2 = Integer.parseInt(o2.substring(0, o2.indexOf("}")));
if (r1 == r2) {
return rulesBySpecListUnordered.indexOf(r1) - rulesBySpecListUnordered.indexOf(r2);
}
return r1 - r2;
}
});
for (int i = 0; i < rulesBySpecList.size(); i++) {
rulesBySpecList.set(i, rulesBySpecList.get(i).substring(rulesBySpecList.get(i).indexOf("}") + 1));
}
styleStr = String.join(";", rulesBySpecList) + ";";
String[] styleDefs = styleStr.split(";");
for (String styleDef : styleDefs) {
if (!styleDef.contains(":")) {
continue;
@@ -75,7 +111,6 @@ class SvgStyle {
}
}
}
}
return styleValues;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
package com.jpexs.decompiler.flash.importers.svg.css;
/**
*
* @author JPEXS
*/
public class CssParseException extends Exception {
public CssParseException(String s) {
super(s);
}
public CssParseException() {
}
}

View File

@@ -0,0 +1,41 @@
package com.jpexs.decompiler.flash.importers.svg.css;
/**
*
* @author JPEXS
*/
public class CssParsedSymbol {
public String value;
public CssSymbolType type;
public CssParsedSymbol(String value, CssSymbolType type) {
this.value = value;
this.type = type;
}
public boolean isType(String... ss){
if(type == CssSymbolType.OTHER){
for(String s:ss){
if(s.equals(value)){
return true;
}
}
}
return false;
}
public boolean isType(CssSymbolType... types){
for(CssSymbolType type:types){
if(this.type == type){
return true;
}
}
return false;
}
@Override
public String toString() {
return type.toString() + ": " + value;
}
}

View File

@@ -0,0 +1,477 @@
package com.jpexs.decompiler.flash.importers.svg.css;
import com.jpexs.helpers.Reference;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
*
* CSS Stylesheet parser. Based on https://www.w3.org/TR/CSS21/grammar.html
*
* @author JPEXS
*/
public class CssParser {
private final CssLexer lexer;
private final String s;
private final List<String> selectors = new ArrayList<>();
private final List<String> declarations = new ArrayList<>();
private final List<List<String>> propNames = new ArrayList<>();
private final List<List<String>> propValues = new ArrayList<>();
private final List<Integer> specifities = new ArrayList<>();
public CssParser(String s) {
this.s = s;
this.lexer = new CssLexer(new StringReader(s));
}
public void styleshet() throws IOException, CssParseException {
CssParsedSymbol symb = lex();
if (symb.type == CssSymbolType.CHARSET_SYM) {
expect(CssSymbolType.STRING);
expect(";");
symb = lex();
}
while (symb.isType(CssSymbolType.S, CssSymbolType.CDO, CssSymbolType.CDC)) {
symb = lex();
}
while (symb.type == CssSymbolType.IMPORT_SYM) {
sstar();
expect(CssSymbolType.STRING, CssSymbolType.URI);
sstar();
symb = lex();
if (!symb.isType(";")) {
lexer.pushback(symb);
media_list();
expect(";");
}
sstar();
symb = lex();
while (symb.isType(CssSymbolType.CDO, CssSymbolType.CDC)) {
sstar();
symb = lex();
}
}
while (symb.type != CssSymbolType.EOF) {
if (symb.type == CssSymbolType.MEDIA_SYM) {
lexer.pushback(symb);
media();
} else if (symb.type == CssSymbolType.PAGE_SYM) {
lexer.pushback(symb);
List<String> propNames = new ArrayList<>();
List<String> propValues = new ArrayList<>();
page(propNames, propValues);
} else {
lexer.pushback(symb);
if (!ruleset()) {
break;
}
}
symb = lex();
while (symb.isType(CssSymbolType.CDO, CssSymbolType.CDC)) {
sstar();
symb = lex();
}
}
}
private boolean ruleset() throws IOException, CssParseException {
int posSelectorStart = lexer.getPos();
int specifity = selector();
if (specifity == -1) {
return false;
}
CssParsedSymbol symb = lex();
while (symb.isType(",")) {
sstar();
specifity += selector();
symb = lex();
}
expect(symb, "{");
int posSelectorEnd = lexer.getPos() - 1;
String selectorStr = s.substring(posSelectorStart, posSelectorEnd).trim();
selectors.add(selectorStr);
specifities.add(specifity);
int declarationsStart = lexer.getPos();
Reference<String> propName = new Reference<>("");
Reference<String> propValue = new Reference<>("");
List<String> propNames = new ArrayList<>();
List<String> propValues = new ArrayList<>();
sstar();
symb = lex();
if (symb.type == CssSymbolType.IDENT) {
lexer.pushback(symb);
declaration(propName, propValue);
propNames.add(propName.getVal());
propValues.add(propValue.getVal());
symb = lex();
}
while (symb.isType(";")) {
sstar();
symb = lex();
if (symb.type == CssSymbolType.IDENT) {
lexer.pushback(symb);
declaration(propName, propValue);
propNames.add(propName.getVal());
propValues.add(propValue.getVal());
symb = lex();
}
}
expect(symb, "}");
int declarationsEnd = lexer.getPos() - 1;
String declaration = s.substring(declarationsStart, declarationsEnd);
declarations.add(declaration);
this.propNames.add(propNames);
this.propValues.add(propValues);
sstar();
return true;
}
private int selector() throws IOException, CssParseException {
int specifity = simple_selector();
if (specifity == -1) {
return -1;
}
CssParsedSymbol symb = lex();
if (symb.type == CssSymbolType.S) {
while (symb.type == CssSymbolType.S) {
symb = lex();
}
if (symb.isType("+", ">")) {
sstar();
specifity += selector();
} else {
lexer.pushback(symb);
if (symb.isType("*", ".", "[", ":") || symb.isType(CssSymbolType.IDENT, CssSymbolType.HASH)) {
specifity += selector();
}
}
} else if (symb.isType("+", ">")) {
sstar();
specifity += selector();
} else {
lexer.pushback(symb);
}
return specifity;
}
private int simple_selector() throws IOException, CssParseException {
CssParsedSymbol symb = lex();
int specifity = 0;
if (symb.isType(CssSymbolType.IDENT) || symb.isType("*")) {
if (symb.isType(CssSymbolType.IDENT)) {
specifity += 1;
}
while (true) {
symb = lex();
if (symb.type == CssSymbolType.HASH) {
specifity += 100;
} else if (symb.isType(".")) {
expect(CssSymbolType.IDENT);
specifity += 10;
} else if (symb.isType("[")) {
lexer.pushback(symb);
attrib();
specifity += 10;
} else if (symb.isType(":")) {
lexer.pushback(symb);
pseudo();
specifity += 10;
} else {
lexer.pushback(symb);
break;
}
}
} else {
int count = 0;
while (true) {
if (symb.type == CssSymbolType.HASH) {
specifity += 100;
count++;
} else if (symb.isType(".")) {
expect(CssSymbolType.IDENT);
specifity += 10;
count++;
} else if (symb.isType("[")) {
lexer.pushback(symb);
attrib();
specifity += 10;
count++;
} else if (symb.isType(":")) {
lexer.pushback(symb);
pseudo();
specifity += 10;
count++;
} else {
if (count == 0) {
lexer.pushback(symb);
return -1;
}
break;
}
symb = lex();
}
lexer.pushback(symb);
}
return specifity;
}
private void pseudo() throws IOException, CssParseException {
expect(":");
CssParsedSymbol symb = lex();
if (symb.type == CssSymbolType.IDENT) {
//okay
} else if (symb.type == CssSymbolType.FUNCTION) {
sstar();
symb = lex();
if (symb.type == CssSymbolType.IDENT) {
sstar();
symb = lex();
}
expect(symb, ")");
}
}
private void attrib() throws IOException, CssParseException {
expect("[");
sstar();
expect(CssSymbolType.IDENT);
sstar();
CssParsedSymbol symb = lex();
if (symb.isType(CssSymbolType.INCLUDES, CssSymbolType.DASHMATCH) || symb.isType("=")) {
sstar();
expect(CssSymbolType.IDENT, CssSymbolType.STRING);
sstar();
symb = lex();
}
expect(symb, "]");
}
private void page(List<String> propNames, List<String> propValues) throws IOException, CssParseException {
expect(CssSymbolType.PAGE_SYM);
sstar();
CssParsedSymbol symb = lex();
if (symb.isType(":")) { //pseudo_page
expect(CssSymbolType.IDENT);
sstar();
symb = lex();
}
expect(symb, "{");
sstar();
Reference<String> propName = new Reference<>("");
Reference<String> propValue = new Reference<>("");
declaration(propName, propValue);
propNames.add(propName.getVal());
propValues.add(propValue.getVal());
symb = lex();
while (symb.isType(";")) {
sstar();
symb = lex();
if (!symb.isType(";", "}")) {
declaration(propName, propValue);
propNames.add(propName.getVal());
propValues.add(propValue.getVal());
}
}
expect(symb, "}");
}
private void declaration(Reference<String> propName, Reference<String> propValue) throws IOException, CssParseException {
CssParsedSymbol symb = lex();
expect(symb, CssSymbolType.IDENT);
propName.setVal(symb.value);
sstar();
expect(":");
sstar();
lexer.startBuffer();
expr();
symb = lex();
if (symb.type == CssSymbolType.IMPORTANT_SYM) {
sstar();
} else {
lexer.pushback(symb);
}
propValue.setVal(lexer.getAndClearBuffer());
}
private void expr() throws IOException, CssParseException {
term(true);
while (true) {
CssParsedSymbol symb = lex();
if (symb.isType("/", ",")) {
sstar();
term(true);
} else {
lexer.pushback(symb);
if (!term(false)) {
break;
}
}
}
}
private boolean term(boolean required) throws IOException, CssParseException {
CssParsedSymbol symb = lex();
if (symb.isType("-", "+")) {
symb = lex();
}
if (symb.isType(CssSymbolType.NUMBER, CssSymbolType.PERCENTAGE,
CssSymbolType.LENGTH, CssSymbolType.EMS, CssSymbolType.EXS,
CssSymbolType.ANGLE, CssSymbolType.TIME, CssSymbolType.FREQ)) {
sstar();
} else if (symb.isType(CssSymbolType.STRING, CssSymbolType.IDENT, CssSymbolType.URI)) {
sstar();
} else if (symb.type == CssSymbolType.HASH) {
sstar();
} else if (symb.type == CssSymbolType.FUNCTION) {
sstar();
expr();
expect(")");
sstar();
} else {
lexer.pushback(symb);
if (required) {
throw new CssParseException();
}
return false;
}
return true;
}
private void media() throws IOException, CssParseException {
expect(CssSymbolType.MEDIA_SYM);
sstar();
media_list();
expect("{");
sstar();
CssParsedSymbol symb = lex();
while (!symb.isType("}")) {
if (!ruleset()) {
break;
}
symb = lex();
}
sstar();
}
private void medium() throws IOException, CssParseException {
expect(CssSymbolType.IDENT);
sstar();
}
private void media_list() throws IOException, CssParseException {
medium();
CssParsedSymbol symb = lex();
while (symb.isType(",")) {
sstar();
medium();
}
lexer.pushback(symb);
}
private void sstar() throws IOException {
CssParsedSymbol symb = lex();
while (symb.type == CssSymbolType.S) {
symb = lex();
}
lexer.pushback(symb);
}
private CssParsedSymbol lex() throws IOException {
CssParsedSymbol v = lexer.lex();
//System.err.println("" + v);
return v;
}
private void expect(String s) throws IOException, CssParseException {
CssParsedSymbol symb = lex();
if (symb.type != CssSymbolType.OTHER) {
throw new CssParseException(s + " expected but " + symb + " found");
}
if (!s.equals(symb.value)) {
throw new CssParseException(s + " expected but " + symb + " found");
}
}
private void expect(CssParsedSymbol symb, String s) throws IOException, CssParseException {
if (symb.type != CssSymbolType.OTHER) {
throw new CssParseException(s + " expected but " + symb + " found");
}
if (!s.equals(symb.value)) {
throw new CssParseException(s + " expected but " + symb.value + " found");
}
}
private void expect(CssParsedSymbol symb, CssSymbolType... types) throws IOException, CssParseException {
List<String> toPrint = new ArrayList<>();
for (CssSymbolType type : types) {
toPrint.add(type.toString());
if (symb.type == type) {
return;
}
}
throw new CssParseException(String.join(",", toPrint) + " expected but " + symb + " found");
}
private void expect(CssSymbolType... types) throws IOException, CssParseException {
List<String> toPrint = new ArrayList<>();
CssParsedSymbol symb = lex();
for (CssSymbolType type : types) {
toPrint.add(type.toString());
if (symb.type == type) {
return;
}
}
throw new CssParseException(String.join(",", toPrint) + " expected but " + symb + " found");
}
public int getCountRulesets() {
return declarations.size();
}
public String getSelector(int index) {
return selectors.get(index);
}
public String getDeclarations(int index) {
return declarations.get(index);
}
public int getPropertyCount(int index) {
return propNames.get(index).size();
}
public String getPropertyName(int ruleSetIndex, int propertyIndex) {
return propNames.get(ruleSetIndex).get(propertyIndex);
}
public String getPropertyValue(int ruleSetIndex, int propertyIndex) {
return propValues.get(ruleSetIndex).get(propertyIndex);
}
public int getSpecifity(int index) {
return specifities.get(index);
}
}

View File

@@ -0,0 +1,686 @@
package com.jpexs.decompiler.flash.importers.svg.css;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
* @author JPEXS
*
* Based on implementation https://github.com/css2xpath/css2xpath
*
*/
public class CssSelectorToXPath {
private String xpath_to_lower(String s) {
return "translate("
+ (s == null ? "normalize-space()" : s)
+ ", 'ABCDEFGHJIKLMNOPQRSTUVWXYZ'"
+ ", 'abcdefghjiklmnopqrstuvwxyz')";
}
private String xpath_ends_with(String s1, String s2) {
return "substring(" + s1 + ","
+ "string-length(" + s1 + ")-string-length(" + s2 + ")+1)=" + s2;
}
private String xpath_url(String s) {
return "substring-before(concat(substring-after("
+ (s == null ? xpath_url_attrs : s) + ",\"://\"),\"?\"),\"?\")";
}
private String xpath_url_path(String s) {
return "substring-after(" + (s == null || s.isEmpty() ? xpath_url_attrs : s) + ",\"/\")";
}
private String xpath_url_domain(String s) {
return "substring-before(concat(substring-after("
+ (s == null || s.isEmpty() ? xpath_url_attrs : s) + ",\"://\"),\"/\"),\"/\")";
}
private final String xpath_url_attrs = "@href|@src";
private final String xpath_lower_case = xpath_to_lower(null);
private final String xpath_ns_uri = "ancestor-or-self::*[last()]/@url";
private final String xpath_ns_path = xpath_url_path(xpath_url(xpath_ns_uri));
private final String xpath_has_protocal = "(starts-with(" + xpath_url_attrs + ",\"http://\") or starts-with(" + xpath_url_attrs + ",\"https://\"))";
private final String xpath_is_internal = "starts-with(" + xpath_url(null) + "," + xpath_url_domain(xpath_ns_uri) + ") or " + xpath_ends_with(xpath_url_domain(null), xpath_url_domain(xpath_ns_uri));
private final String xpath_is_local = "(" + xpath_has_protocal + " and starts-with(" + xpath_url(null) + "," + xpath_url(xpath_ns_uri) + "))";
private final String xpath_is_path = "starts-with(" + xpath_url_attrs + ",\"/\")";
private final String xpath_is_local_path = "starts-with(" + xpath_url_path(null) + "," + xpath_ns_path + ")";
private final String xpath_normalize_space = "normalize-space()";
private final String xpath_internal = "[not(" + xpath_has_protocal + ") or " + xpath_is_internal + "]";
private final String xpath_external = "[" + xpath_has_protocal + " and not(" + xpath_is_internal + ")]";
private final char escape_literal = (char) 30;
private final char escape_parens = (char) 31;
private final String regex_string_literal = "(\"[^\"\\x1E]*\"|'[^'\\x1E]*'|=\\s*[^\\s\\]\\'\\\"]+)"; // /g
private final String regex_escaped_literal = "['\"]?(\\x1E+)['\"]?"; // /g;
private final String regex_css_wrap_pseudo = "(\\x1F\\)|[^\\)])\\:(first|limit|last|gt|lt|eq|nth)([^\\-]|$)"; // bez g
private final String regex_specal_chars = "[\\x1C-\\x1F]+"; // /g;
private final String regex_first_axis = "^([\\s\\(\\x1F]*)(\\.?[^\\.\\/\\(]{1,2}[a-z]*:*)";
private final String regex_filter_prefix = "(^|\\/|\\:)\\["; // /g;
private final String regex_attr_prefix = "([^\\(\\[\\/\\|\\s\\x1F])\\@"; // /g;
private final String regex_nth_equation = "^([-0-9]*)n.*?([0-9]*)$"; //bez g
private final String css_combinators_regex = "\\s*(!?[+>~,^ ])\\s*(\\.?\\/+|[a-z\\-]+::)?([a-z\\-]+\\()?((and\\s*|or\\s*|mod\\s*)?[^+>~,\\s'\"\\]\\|\\^\\$\\!\\<\\=\\x1C-\\x1F]+)?"; // /g;
// Check if string is numeric
private boolean isNumeric(String s) {
try {
Integer.parseInt(s);
} catch (NumberFormatException nfe) {
return false;
}
return true;
}
private String css_combinators_callback(String match, String operator, String axis, String func, String literal, String exclude, int offset, String orig) {
String prefix = ""; // If we can, we'll prefix a '.'
// XPath operators can look like node-name selectors
// Detect false positive for " and", " or", " mod"
if (operator.equals(" ") && exclude != null) {
return match;
}
if (axis == null) {
// Only allow node-selecting XPath functions
// Detect false positive for " + count(...)", " count(...)", " > position()", etc.
if (func != null && (!func.equals("node(") && !func.equals("text(") && !func.equals("comment("))) {
return null;
} else if (literal == null) {
literal = func;
} // Handle case " + text()", " > comment()", etc. where "func" is our "literal"
// XPath math operators match some CSS combinators
// Detect false positive for " + 1", " > 1", etc.
if (isNumeric(literal)) {
return match;
}
if (orig.length() <= offset - 1) {
prefix = ".";
} else {
char prevChar = orig.charAt(offset - 1);
if (prevChar == '('
|| prevChar == '|'
|| prevChar == ':') {
prefix = ".";
}
}
}
// Return if we don't have a selector to follow the axis
if (literal == null) {
if (offset + match.length() == orig.length()) {
literal = "*";
} else {
return match;
}
}
switch (operator) {
case " ":
return "//" + literal;
case ">":
return "/" + literal;
case "+":
return prefix + "/following-sibling::*[1]/self::" + literal;
case "~":
return prefix + "/following-sibling::" + literal;
case ",":
if (axis == null) {
}
axis = ".//";
return "|" + axis + literal;
case "^": // first child
return "/child::*[1]/self::" + literal;
case "!^": // last child
return "/child::*[last()]/self::" + literal;
case "! ": // ancestor-or-self
return "/ancestor-or-self::" + literal;
case "!>": // direct parent
return "/parent::" + literal;
case "!+": // adjacent preceding sibling
return "/preceding-sibling::*[1]/self::" + literal;
case "!~": // preceding sibling
return "/preceding-sibling::" + literal;
// case '~~'
// return '/following-sibling::*/self::|'+selectorStart(orig, offset)+'/preceding-sibling::*/self::'+literal;
}
return null;
}
private final String css_attributes_regex = "\\[([^\\@\\|\\*\\=\\^\\~\\$\\!\\(\\/\\s\\x1C-\\x1F]+)\\s*(([\\|\\*\\~\\^\\$\\!]?)=?\\s*(\\x1E+))?\\]"; // /g;
private String css_attributes_callback(String str, String attr, String comp, String op, String val, int offset, String orig) {
String axis = "";
//String prevChar = offset == 0 ? "" : "" + orig.charAt(offset - 1);
/*
if (prevChar === '/' || // found after an axis shortcut ("/", "//", etc.)
prevChar === ':') // found after an axis ("self::", "parent::", etc.)
axis = '*';*/
if (op == null) {
op = "";
}
switch (op) {
case "!":
return axis + "[not(@" + attr + ") or @" + attr + "!=\"" + val + "\"]";
case "$":
return axis + "[substring(@" + attr + ",string-length(@" + attr + ")-(string-length(\"" + val + "'\")-1))=\"" + val + "\"]";
case "^":
return axis + "[starts-with(@" + attr + ",\"" + val + "\")]";
case "~":
return axis + "[contains(concat(\" \",normalize-space(@" + attr + "),\" \"),concat(\" \",\"" + val + "\",\" \"))]";
case "*":
return axis + "[contains(@" + attr + ",\"" + val + "\")]";
case "|":
return axis + "[@" + attr + "=\"" + val + "\" or starts-with(@" + attr + ",concat(\"" + val + "\",\"-\"))]";
default:
if (comp == null) {
if (attr.charAt(attr.length() - 1) == '(' || attr.matches("^[0-9]+$") || attr.indexOf(':') != -1) {
return str;
}
return axis + "[@" + attr + "]";
} else {
return axis + "[@" + attr + "=\"" + val + "\"]";
}
}
}
private final String css_pseudo_classes_regex = ":([a-z\\-]+)(\\((\\x1F+)(([^\\x1F]+(\\3\\x1F+)?)*)(\\3\\)))?"; // /g;
private String css_pseudo_classes_callback(String match, String name, String g1, String g2, String arg, String g3, String g4, String g5, int offset, String orig) {
if ((offset - 2 >= 0) && orig.charAt(offset - 1) == ':' && orig.charAt(offset - 2) != ':') {
// XPath "axis::node-name" will match
// Detect false positive ":node-name"
return match;
}
if ("odd".equals(name) || "even".equals(name)) {
arg = name;
name = "nth-of-type";
}
switch (name) { // name.toLowerCase()?
case "after":
return "[count(" + css2xpath("preceding::" + arg, true) + ") > 0]";
case "after-sibling":
return "[count(" + css2xpath("preceding-sibling::" + arg, true) + ") > 0]";
case "before":
return "[count(" + css2xpath("following::" + arg, true) + ") > 0]";
case "before-sibling":
return "[count(" + css2xpath("following-sibling::" + arg, true) + ") > 0]";
case "checked":
return "[@selected or @checked]";
case "contains":
return "[contains(" + xpath_normalize_space + "," + arg + ")]";
case "icontains":
return "[contains(" + xpath_lower_case + "," + xpath_to_lower(arg) + ")]";
case "empty":
return "[not(*) and not(normalize-space())]";
case "enabled":
case "disabled":
return "[@" + name + "]";
case "first-child":
return "[not(preceding-sibling::*)]";
case "first":
case "limit":
case "first-of-type":
if (arg != null) {
return "[position()<=" + arg + "]";
}
return "[1]";
case "gt":
// Position starts at 0 for consistency with Sizzle selectors
return "[position()>" + (Integer.parseInt(arg, 10) + 1) + "]";
case "lt":
// Position starts at 0 for consistency with Sizzle selectors
return "[position()<" + (Integer.parseInt(arg, 10) + 1) + "]";
case "last-child":
return "[not(following-sibling::*)]";
case "only-child":
return "[not(preceding-sibling::*) and not(following-sibling::*)]";
case "only-of-type":
return "[not(preceding-sibling::*[name()=name(self::node())]) and not(following-sibling::*[name()=name(self::node())])]";
case "nth-child":
if (isNumeric(arg)) {
return "[(count(preceding-sibling::*)+1) = " + arg + "]";
}
switch (arg) {
case "even":
return "[(count(preceding-sibling::*)+1) mod 2=0]";
case "odd":
return "[(count(preceding-sibling::*)+1) mod 2=1]";
default:
String[] a = (arg == null || arg.isEmpty() ? "0" : arg).replaceAll(regex_nth_equation, "$1+$2").split("\\+");
String a0;
if (a.length < 1 || a[0].isEmpty()) {
a0 = "1";
} else {
a0 = a[0];
}
String a1;
if (a.length < 2 || a[1].isEmpty()) {
a1 = "0";
} else {
a1 = a[1];
}
return "[(count(preceding-sibling::*)+1)>=" + a1 + " and ((count(preceding-sibling::*)+1)-" + a1 + ") mod " + a0 + "=0]";
}
case "nth-of-type":
if (isNumeric(arg)) {
return "[" + arg + "]";
}
switch (arg) {
case "odd":
return "[position() mod 2=1]";
case "even":
return "[position() mod 2=0 and position()>=0]";
default:
String[] a = (arg == null || arg.isEmpty() ? "0" : arg).replaceAll(regex_nth_equation, "$1+$2").split("\\+");
String a0;
if (a.length < 1 || a[0].isEmpty()) {
a0 = "1";
} else {
a0 = a[0];
}
String a1;
if (a.length < 2 || a[1].isEmpty()) {
a1 = "0";
} else {
a1 = a[1];
}
return "[position()>=" + a1 + " and (position()-" + a1 + ") mod " + a0 + "=0]";
}
case "eq":
case "nth":
// Position starts at 0 for consistency with Sizzle selectors
if (isNumeric(arg)) {
return "[" + (Integer.parseInt(arg, 10) + 1) + "]";
}
return "[1]";
case "text":
return "[@type=\"text\"]";
case "istarts-with":
return "[starts-with(" + xpath_lower_case + "," + xpath_to_lower(arg) + ")]";
case "starts-with":
return "[starts-with(" + xpath_normalize_space + "," + arg + ")]";
case "iends-with":
return "[" + xpath_ends_with(xpath_lower_case, xpath_to_lower(arg)) + "]";
case "ends-with":
return "[" + xpath_ends_with(xpath_normalize_space, arg) + "]";
case "has":
String xpath1 = prependAxis(css2xpath(arg, true), ".//");
return "[count(" + xpath1 + ") > 0]";
case "has-sibling":
String xpath2 = css2xpath("preceding-sibling::" + arg, true);
return "[count(" + xpath2 + ") > 0 or count(following-sibling::" + xpath2.substring(19) + ") > 0]";
case "has-parent":
return "[count(" + css2xpath("parent::" + arg, true) + ") > 0]";
case "has-ancestor":
return "[count(" + css2xpath("ancestor::" + arg, true) + ") > 0]";
case "last":
case "last-of-type":
if (arg != null) {
return "[position()>last()-" + arg + "]";
}
return "[last()]";
case "selected": // Sizzle: "(option) elements that are currently selected"
return "[local-name()=\"option\" and @selected]";
case "skip":
case "skip-first":
return "[position()>" + arg + "]";
case "skip-last":
if (arg != null) {
return "[last()-position()>=" + arg + "]";
}
return "[position()<last()]";
case "root":
return "/ancestor::[last()]";
case "range":
String arr[] = arg.split(",");
return "[" + arr[0] + "<=position() and position()<=" + arr[1] + "]";
case "input": // Sizzle: "input, button, select, and textarea are all considered to be input elements."
return "[local-name()=\"input\" or local-name()=\"button\" or local-name()=\"select\" or local-name()=\"textarea\"]";
case "internal":
return xpath_internal;
case "external":
return xpath_external;
case "http":
case "https":
case "mailto":
case "javascript":
return "[starts-with(@href,concat(\"" + name + "\",\":\"))]";
case "domain":
return "[(string-length(" + xpath_url_domain(null) + ")=0 and contains(" + xpath_url_domain(xpath_ns_uri) + "," + arg + ")) or contains(" + xpath_url_domain(null) + "," + arg + ")]";
case "path":
return "[starts-with(" + xpath_url_path(null) + ",substring-after(\"" + arg + "\",\"/\"))]";
case "not":
String xpath3 = css2xpath(arg, true);
if (xpath3.charAt(0) == '[') {
xpath3 = "self::node()" + xpath3;
}
return "[not(" + xpath3 + ")]";
case "target":
return "[starts-with(@href, \"#\")]";
/*case "root":
return "ancestor-or-self::*[last()]";*/ //FIXME?? Duplicated case label
/* case 'active':
case 'focus':
case 'hover':
case 'link':
case 'visited':
return '';*/
case "lang":
return "[@lang=\"" + arg + "\"]";
case "read-only":
case "read-write":
return "[@" + name.replace("-", "") + "]";
case "valid":
case "required":
case "in-range":
case "out-of-range":
return "[@" + name + "]";
default:
return "[@_pseudo_" + name + "]"; //JPEXS - to disable such notes
}
}
private String css_ids_classes_regex = "(#|\\.)([^\\#\\@\\.\\/\\(\\[\\)\\]\\|\\:\\s\\+\\>\\<\\'\\\"\\x1D-\\x1F]+)"; // /g;
private String css_ids_classes_callback(String str, String op, String val, int offset, String orig) {
String axis = "";
/* var prevChar = orig.charAt(offset-1);
if (prevChar.length === 0 ||
prevChar === '/' ||
prevChar === '(')
axis = '*';
else if (prevChar === ':')
axis = 'node()';*/
if ("#".equals(op)) {
return axis + "[@id=\"" + val + "\"]";
}
return axis + "[contains(concat(\" \",normalize-space(@class),\" \"),\" " + val + " \")]";
}
// Prepend descendant-or-self if no other axis is specified
private String prependAxis(String s, String axis) {
Pattern pat = Pattern.compile(regex_first_axis);
StringBuffer buf = new StringBuffer();
Matcher mat = pat.matcher(s);
while (mat.find()) {
String start = mat.group(1);
String literal = mat.group(2);
if (literal.length() >= 2 && "::".equals(literal.substring(literal.length() - 2))) {
mat.appendReplacement(buf, mat.group());
} else {
if (literal.charAt(0) == '[') {
axis += "*";
}
mat.appendReplacement(buf, start + axis + literal);
}
}
mat.appendTail(buf);
return buf.toString();
}
// Find the begining of the selector, starting at i and working backwards
private int selectorStart(String s, int i) {
int depth = 0;
int offset = 0;
while (i-- > 0) {
switch (s.charAt(i)) {
case ' ':
case escape_parens:
offset++;
break;
case '[':
case '(':
depth--;
if (depth < 0) {
return ++i + offset;
}
break;
case ']':
case ')':
depth++;
break;
case ',':
case '|':
if (depth == 0) {
return ++i + offset;
}
default:
offset = 0;
}
}
return 0;
}
// Append escape "char" to "open" or "close"
private String escapeChar(String s, String open, String close, char chr) {
Pattern pat = Pattern.compile("[\\" + open + "\\" + close + "]");
StringBuffer buf = new StringBuffer();
Matcher mat = pat.matcher(s);
int depth = 0;
while (mat.find()) {
if (open.equals(mat.group())) {
depth++;
}
if (open.equals(mat.group())) {
mat.appendReplacement(buf, mat.group() + repeat("" + chr, depth));
} else {
mat.appendReplacement(buf, repeat("" + chr, depth--) + mat.group());
}
}
mat.appendTail(buf);
return buf.toString();
/*int depth = 0;
return s.replace(new RegExp('[\\' + open + '\\' + close + ']', 'g'), function (a) {
if (a === open) {
depth++;
}
if (a === open) {
return a + repeat(chr, depth);
} else {
return repeat(chr, depth--) + a;
}
})*/
}
private String repeat(String str, int num) {
String result = "";
while (true) {
if ((num & 1) == 1) {
result += str;
}
num >>>= 1;
if (num <= 0) {
break;
}
str += str;
}
return result;
}
private static interface ReplaceCallBack {
public String run(Matcher mat, String s);
}
private String replace(String s, String regexp, ReplaceCallBack callback) {
Pattern pat = Pattern.compile(regexp);
StringBuffer sb = new StringBuffer();
Matcher m = pat.matcher(s);
while (m.find()) {
m.appendReplacement(sb, callback.run(m, s));
}
m.appendTail(sb);
return sb.toString();
}
private int search(String s, String regexp) {
Pattern pat = Pattern.compile(regexp);
Matcher m = pat.matcher(s);
if (m.find()) {
return m.start();
}
return -1;
}
public String css2xpath(String s) {
return css2xpath(s, false);
}
public String css2xpath(String s, boolean nested) {
if (nested == true) {
// Replace :pseudo-classes
final String s1 = s;
s = replace(s, css_pseudo_classes_regex, new ReplaceCallBack() {
@Override
public String run(Matcher mat, String s) {
return css_pseudo_classes_callback(mat.group(), mat.group(1), mat.group(1), mat.group(2), mat.group(3), mat.group(4), mat.group(5), mat.group(6), mat.start(), s);
}
});
s = replace(s, css_pseudo_classes_regex, new ReplaceCallBack() {
@Override
public String run(Matcher mat, String s) {
return css_pseudo_classes_callback(mat.group(), mat.group(1), mat.group(2), mat.group(3), mat.group(4),
mat.group(5), mat.group(6), mat.group(7), mat.start(), s);
}
});
// Replace #ids and .classes
s = replace(s, css_ids_classes_regex, new ReplaceCallBack() {
@Override
public String run(Matcher mat, String s) {
return css_ids_classes_callback(mat.group(), mat.group(1), mat.group(2), mat.start(), s);
}
});
return s;
}
// Tag open and close parenthesis pairs (for RegExp searches)
s = escapeChar(s, "(", ")", escape_parens);
// Remove and save any string literals
List<String> literals = new ArrayList<>();
s = replace(s, regex_string_literal, new ReplaceCallBack() {
@Override
public String run(Matcher mat, String fullS) {
String s = mat.group();
String a = mat.group(1);
if (a.charAt(0) == '=') {
a = a.substring(1).trim();
if (isNumeric(a)) {
return s;
}
} else {
a = a.substring(1, a.length() - 1);
}
literals.add(a);
return repeat("" + escape_literal, literals.size());
}
});
// Replace CSS combinators (" ", "+", ">", "~", ",") and reverse combinators ("!", "!+", "!>", "!~")
s = replace(s, css_combinators_regex, new ReplaceCallBack() {
@Override
public String run(Matcher mat, String s) {
return css_combinators_callback(mat.group(), mat.group(1), mat.group(2), mat.group(3), mat.group(4), mat.group(5), mat.start(), s);
}
});
// Replace CSS attribute filters
s = replace(s, css_attributes_regex, new ReplaceCallBack() {
@Override
public String run(Matcher mat, String s) {
return css_attributes_callback(mat.group(), mat.group(1), mat.group(2), mat.group(3), mat.group(4), mat.start(), s);
}
});
// Wrap certain :pseudo-classes in parens (to collect node-sets)
while (true) {
int index = search(s, regex_css_wrap_pseudo);
if (index == -1) {
break;
}
index = s.indexOf(':', index);
int start = selectorStart(s, index);
s = s.substring(0, start)
+ '(' + s.substring(start, index) + ')'
+ s.substring(index);
}
// Replace :pseudo-classes
s = replace(s, css_pseudo_classes_regex, new ReplaceCallBack() {
@Override
public String run(Matcher mat, String s) {
return css_pseudo_classes_callback(mat.group(), mat.group(1), mat.group(2), mat.group(3), mat.group(4), mat.group(5), mat.group(6), mat.group(7), mat.start(), s);
}
});
// Replace #ids and .classes
s = replace(s, css_ids_classes_regex, new ReplaceCallBack() {
@Override
public String run(Matcher mat, String s) {
return css_ids_classes_callback(mat.group(), mat.group(1), mat.group(2), mat.start(), s);
}
});
// Restore the saved string literals
s = replace(s, regex_escaped_literal, new ReplaceCallBack() {
@Override
public String run(Matcher mat, String a) {
String str = literals.get(mat.group(1).length() - 1);
return "\"" + str + "\"";
}
});
// Remove any special characters
s = s.replaceAll(regex_specal_chars, "");
// add * to stand-alone filters
s = s.replaceAll(regex_filter_prefix, "$1*[");
// add "/" between @attribute selectors
s = s.replaceAll(regex_attr_prefix, "$1/@");
/*
Combine multiple filters?
s = escapeChar(s, '[', ']', filter_char);
s = s.replace(/(\x1D+)\]\[\1(.+?[^\x1D])\1\]/g, ' and ($2)$1]')
*/
s = prependAxis(s, ".//"); // prepend ".//" axis to begining of CSS selector
return s;
}
}

View File

@@ -0,0 +1,36 @@
package com.jpexs.decompiler.flash.importers.svg.css;
/**
*
* @author JPEXS
*/
public enum CssSymbolType {
S,
CDO,
CDC,
INCLUDES,
DASHMATCH,
STRING,
BAD_STRING,
IDENT,
HASH,
IMPORT_SYM,
PAGE_SYM,
MEDIA_SYM,
CHARSET_SYM,
IMPORTANT_SYM,
EMS,
EXS,
LENGTH,
ANGLE,
TIME,
FREQ,
DIMENSION,
PERCENTAGE,
NUMBER,
URI,
BAD_URI,
FUNCTION,
EOF,
OTHER
}