1   package org.daisy.pipeline.css.sass.impl;
2   
3   import java.util.regex.Pattern;
4   
5   import com.google.common.base.CaseFormat;
6   
7   import org.daisy.pipeline.css.CssAnalyzer;
8   import org.daisy.pipeline.datatypes.DatatypeRegistry;
9   import org.daisy.pipeline.datatypes.DatatypeService;
10  import org.daisy.pipeline.datatypes.XMLBasedDatatypeService;
11  
12  import org.slf4j.Logger;
13  import org.slf4j.LoggerFactory;
14  
15  import org.unbescape.css.CssEscape;
16  
17  import org.w3c.dom.Element;
18  import org.w3c.dom.Node;
19  
20  class SassVariable implements CssAnalyzer.SassVariable {
21  
22  	private final static Logger logger = LoggerFactory.getLogger(SassVariable.class.getName());
23  
24  	private final String name;
25  	private final String nicename;
26  	private final String description;
27  	private final String value;
28  	private boolean isDefault = true;
29  	private final DatatypeService type;
30  
31  	/**
32  	 * Create a fixed variable.
33  	 */
34  	SassVariable(String name, String value) {
35  		this(name, null, value, null);
36  		isDefault = false;
37  	}
38  
39  	/**
40  	 * Create a writable variable (with a {@code !default} suffix).
41  	 */
42  	SassVariable(String name, Comment precedingComment, String defaultValue, DatatypeRegistry datatypes) {
43  		this.name = name;
44  		DoxygenComment doxygenComment; {
45  			doxygenComment = null;
46  			if (precedingComment != null && precedingComment.text.startsWith("*"))
47  				try {
48  					doxygenComment = DoxygenComment.of(precedingComment);
49  				} catch (IllegalArgumentException e) {
50  					logger.warn("Expected a Doxygen comment but got: " + precedingComment, e);
51  				}
52  		}
53  		String nicename = null;
54  		String description = "";
55  		DatatypeService type = null; {
56  			if (doxygenComment != null) {
57  				if (doxygenComment.varName.isPresent() && !doxygenComment.varName.get().equals(name))
58  					logger.warn("Declaration of variable $" + name + " was preceded by a Doxygen comment " +
59  					            "describing a different variable: " + precedingComment);
60  				else {
61  					nicename = doxygenComment.brief.orElse(null);
62  					description = doxygenComment.body;
63  					if (doxygenComment.type.isPresent()) {
64  						String t = doxygenComment.type.get();
65  						if ("xs:string".equals(t) || "string".equals(t))
66  							type = DatatypeService.XS_STRING;
67  						else if ("xs:integer".equals(t) || "integer".equals(t))
68  							type = DatatypeService.XS_INTEGER;
69  						else if ("xs:nonNegativeInteger".equals(t) || "nonNegativeInteger".equals(t))
70  							type = DatatypeService.XS_NON_NEGATIVE_INTEGER;
71  						else if ("xs:boolean".equals(t) || "boolean".equals(t))
72  							type = DatatypeService.XS_BOOLEAN;
73  						else if ("xs:anyURI".equals(t) || "anyURI".equals(t))
74  							type = DatatypeService.XS_ANY_URI;
75  						else if ("anyFileURI".equals(t))
76  							type = DatatypeService.ANY_FILE_URI;
77  						else if ("anyDirURI".equals(t))
78  							type = DatatypeService.ANY_DIR_URI;
79  						else {
80  							type = datatypes != null ? datatypes.getDatatype(t).orNull() : null;
81  							if (type == null)
82  								try {
83  									throw new IllegalArgumentException(
84  										"Invalid type in variable declaration '" + type + "': does not match a known data type");
85  								} catch (IllegalArgumentException e) {
86  									logger.warn("Invalid type in Doxygen comment: " + precedingComment, e);
87  								}
88  						}
89  					} else if (doxygenComment.typeDef.isPresent()) {
90  						Element def = doxygenComment.typeDef.get();
91  						type = new XMLBasedDatatypeService() { // id will be filled in later by DatatypeRegistry
92  							@Override
93  							protected Node readDocument() throws Exception {
94  								return def;
95  							}
96  						};
97  						try {
98  							// check if type declaration is valid
99  							type.validate("x"); // readTypeDecl() called lazily
100 						} catch (RuntimeException e) {
101 							type = null;
102 							logger.warn("Invalid type declaration in Doxygen comment: " + precedingComment, e);
103 						}
104 						if (type != null && datatypes != null)
105 							datatypes.registerVolatile(type);
106 					}
107 				}
108 			} else if (precedingComment != null)
109 				description = Pattern.compile("^\\s*\\*", Pattern.MULTILINE)
110 				                     .matcher(precedingComment.text)
111 				                     .replaceAll("")
112 				                     .replaceAll("\\s+", " ")
113 				                     .trim();
114 		}
115 		if (nicename == null)
116 			nicename = name.substring(0, 1).toUpperCase()
117 				+ CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, name)
118 				                         .replace('_', ' ')
119 				                         .replace('-', ' ')
120 				                         .toLowerCase()
121 				                         .substring(1);
122 		if (type == null) {
123 			// infer from default value
124 			if ("true".equals(defaultValue) || "false".equals(defaultValue))
125 				type = DatatypeService.XS_BOOLEAN;
126 			else {
127 				try {
128 					Integer.parseInt(defaultValue);
129 					type = DatatypeService.XS_INTEGER;
130 				} catch (NumberFormatException e) {
131 					type = DatatypeService.XS_STRING;
132 				}
133 			}
134 		}
135 		if (type == DatatypeService.XS_STRING) {
136 			if (defaultValue.startsWith("\"") || defaultValue.startsWith("'")) {
137 				if ((defaultValue.startsWith("\"") && !defaultValue.endsWith("\""))
138 				    || (defaultValue.startsWith("'") && !defaultValue.endsWith("'")))
139 					throw new IllegalArgumentException("Invalid value in variable declaration: " + defaultValue);
140 				defaultValue = CssEscape.unescapeCss(defaultValue.substring(1, defaultValue.length() - 1));
141 			}
142 		}
143 		this.nicename = nicename;
144 		this.description = description;
145 		this.value = defaultValue;
146 		this.type = type;
147 	}
148 
149 	@Override
150 	public String getName() {
151 		return name;
152 	}
153 
154 	@Override
155 	public String getNiceName() {
156 		return nicename;
157 	}
158 
159 	@Override
160 	public String getDescription() {
161 		return description;
162 	}
163 
164 	@Override
165 	public String getValue() {
166 		return value;
167 	}
168 
169 	@Override
170 	public boolean isDefault() {
171 		return isDefault;
172 	}
173 
174 	@Override
175 	public DatatypeService getType() {
176 		return type;
177 	}
178 
179 	@Override
180 	public String toString() {
181 		return "$" + name + ": " + value + (isDefault ? " !default" : "") + ";";
182 	}
183 }