1   package org.daisy.pipeline.css.impl;
2   
3   import java.util.Collection;
4   import java.util.Collections;
5   import java.util.List;
6   import java.util.Map;
7   
8   import javax.xml.namespace.QName;
9   import javax.xml.stream.XMLStreamException;
10  import javax.xml.transform.URIResolver;
11  
12  import com.google.common.collect.Iterables;
13  
14  import cz.vutbr.web.css.CSSProperty.CounterIncrement;
15  import cz.vutbr.web.css.CSSProperty.CounterReset;
16  import cz.vutbr.web.css.CSSProperty.CounterSet;
17  import cz.vutbr.web.css.NodeData;
18  import cz.vutbr.web.css.RuleFactory;
19  import cz.vutbr.web.css.Selector.PseudoElement;
20  import cz.vutbr.web.css.Term;
21  import cz.vutbr.web.css.TermFunction;
22  import cz.vutbr.web.css.TermIdent;
23  import cz.vutbr.web.css.TermList;
24  import cz.vutbr.web.css.TermPair;
25  import cz.vutbr.web.csskit.antlr.CSSParserFactory;
26  import cz.vutbr.web.csskit.RuleFactoryImpl;
27  import cz.vutbr.web.domassign.StyleMap;
28  
29  import org.daisy.braille.css.BrailleCSSParserFactory;
30  import org.daisy.braille.css.BrailleCSSProperty.Content;
31  import org.daisy.braille.css.BrailleCSSProperty.Display;
32  import org.daisy.braille.css.BrailleCSSProperty.ListStyleType;
33  import org.daisy.braille.css.RuleCounterStyle;
34  import org.daisy.braille.css.SupportedBrailleCSS;
35  import org.daisy.common.stax.BaseURIAwareXMLStreamWriter;
36  import org.daisy.common.stax.XMLStreamWriterHelper;
37  import org.daisy.common.transform.XMLTransformer;
38  import org.daisy.pipeline.css.CounterEvaluator;
39  import org.daisy.pipeline.css.CounterStyle;
40  import org.daisy.pipeline.css.CssCascader;
41  import org.daisy.pipeline.css.CssPreProcessor;
42  import org.daisy.pipeline.css.CssSerializer;
43  import org.daisy.pipeline.css.JStyleParserCssCascader;
44  import org.daisy.pipeline.css.Medium;
45  import org.daisy.pipeline.css.XsltProcessor;
46  
47  import org.osgi.service.component.annotations.Component;
48  
49  import org.slf4j.Logger;
50  import org.slf4j.LoggerFactory;
51  
52  import org.w3c.dom.Element;
53  import org.w3c.dom.Node;
54  
55  /**
56   * For now this cascader handles only 'print' and 'screen' media without support for counters
57   * ({@code (-counter-support: none)}). It will generate marker contents by <a
58   * href="https://www.w3.org/TR/css-lists-3/#content-property">evaluating marker contents</a>, based
59   * on {@code ::marker} pseudo-element rules, 'list-style-type', 'counter-reset', 'counter-set' and
60   * 'counter-increment' properties and {@code @counter-style} rules, according to <a
61   * href="https://www.w3.org/TR/css-lists-3/#content-property">CSS Lists and Counters Module Level
62   * 3</a>. The marker contents are inserted as text nodes.
63   *
64   * For now all other styles are ignored and no attributes are added.
65   */
66  @Component(
67  	name = "DefaultCssCascader",
68  	service = { CssCascader.class }
69  )
70  public class DefaultCssCascader implements CssCascader {
71  
72  	public boolean supportsMedium(Medium medium) {
73  		switch (medium.getType()) {
74  		case PRINT:
75  		case SCREEN:
76  			return "none".equals(medium.getCustomFeatures().get("counter-support"));
77  		default:
78  			return false;
79  		}
80  	}
81  
82  	public XMLTransformer newInstance(Medium medium,
83  	                                  String userStylesheet,
84  	                                  URIResolver uriResolver,
85  	                                  CssPreProcessor preProcessor,
86  	                                  XsltProcessor xsltProcessor,
87  	                                  QName attributeName,
88  	                                  boolean multipleAttrs) {
89  		if (!supportsMedium(medium))
90  			throw new IllegalArgumentException("medium not supported: " + medium);
91  		return new Transformer(uriResolver, preProcessor, xsltProcessor, userStylesheet, medium,
92  		                       attributeName, multipleAttrs);
93  	}
94  
95  	// using braille-css because @counter-style is not supported by jStyleParser
96  	private static final CSSParserFactory parserFactory = new BrailleCSSParserFactory();
97  	private static final RuleFactory ruleFactory = RuleFactoryImpl.getInstance();
98  	private static final SupportedBrailleCSS supportedCSS = new SupportedBrailleCSS(false, true); // FIXME: support "list-style" shorthand
99  
100 	/*
101 	 * We can make use of JStyleParserCssCascader for the evaluation of marker contents because to
102 	 * compute the value we only need to know about preceding and ancestor nodes, which is a
103 	 * condition that is fulfilled because of how traverse() is called.
104 	 */
105 	private static class Transformer extends JStyleParserCssCascader {
106 
107 		private Map<String,CounterStyle> namedCounterStyles = null;
108 		private CounterEvaluator<Element> evaluator;
109 		private final QName markerAttributeName;
110 		private final QName markerContentAttributeName;
111 
112 		private Transformer(URIResolver resolver, CssPreProcessor preProcessor, XsltProcessor xsltProcessor,
113 		                    String userStyleSheet, Medium medium, QName attributeName, boolean multipleAttrs) {
114 			super(resolver, preProcessor, xsltProcessor, userStyleSheet, medium, attributeName,
115 			      parserFactory, ruleFactory, supportedCSS, supportedCSS);
116 			if (attributeName == null) {
117 				markerAttributeName= null;
118 				markerContentAttributeName = null;
119 			} else if (multipleAttrs) {
120 				markerAttributeName= null;
121 				markerContentAttributeName
122 					= new QName(attributeName.getNamespaceURI(), "marker-content", attributeName.getPrefix());
123 			} else {
124 				markerAttributeName= attributeName;
125 				markerContentAttributeName = null;
126 			}
127 		}
128 
129 		@Override
130 		protected void processElement(Element element, StyleMap styleMap, BaseURIAwareXMLStreamWriter writer)
131 				throws XMLStreamException, IllegalArgumentException {
132 			if (evaluator == null)
133 				// styleMap is constant so we can define evaluator based on styleMap argument of first call
134 				evaluator = new CounterEvaluator<Element>() {
135 						@Override
136 						protected Collection<TermPair<String,Integer>> getCounterReset(Element element) {
137 							NodeData style = styleMap.get(element);
138 							if (style.getProperty("counter-reset", false) == CounterReset.list_values) {
139 								List<Term<?>> list = style.getValue(TermList.class, "counter-reset", false);
140 								if (list != null) {
141 									for (Term<?> t : list) {
142 										if (!(t instanceof TermPair))
143 											throw new IllegalStateException("coding error");
144 										TermPair p = (TermPair)t;
145 										if (!(p.getKey() instanceof String))
146 											throw new IllegalStateException("coding error");
147 										if (!(p.getValue() instanceof Integer))
148 											throw new IllegalStateException("coding error");
149 									}
150 									return (List<TermPair<String,Integer>>)(List<?>)list;
151 								}
152 							}
153 							return null;
154 						}
155 						@Override
156 						protected Collection<TermPair<String,Integer>> getCounterSet(Element element) {
157 							NodeData style = styleMap.get(element);
158 							if (style.getProperty("counter-set", false) == CounterSet.list_values) {
159 								List<Term<?>> list = style.getValue(TermList.class, "counter-set", false);
160 								if (list != null) {
161 									for (Term<?> t : list) {
162 										if (!(t instanceof TermPair))
163 											throw new IllegalStateException("coding error");
164 										TermPair p = (TermPair)t;
165 										if (!(p.getKey() instanceof String))
166 											throw new IllegalStateException("coding error");
167 										if (!(p.getValue() instanceof Integer))
168 											throw new IllegalStateException("coding error");
169 									}
170 									return (List<TermPair<String,Integer>>)(List<?>)list;
171 								}
172 							}
173 							return null;
174 						}
175 						@Override
176 						protected Collection<TermPair<String,Integer>> getCounterIncrement(Element element) {
177 							NodeData style = styleMap.get(element);
178 							if (style.getProperty("counter-increment", false) == CounterIncrement.list_values) {
179 								List<Term<?>> list = style.getValue(TermList.class, "counter-increment", false);
180 								if (list != null) {
181 									for (Term<?> t : list) {
182 										if (!(t instanceof TermPair))
183 											throw new IllegalStateException("coding error");
184 										TermPair p = (TermPair)t;
185 										if (!(p.getKey() instanceof String))
186 											throw new IllegalStateException("coding error");
187 										if (!(p.getValue() instanceof Integer))
188 											throw new IllegalStateException("coding error");
189 									}
190 									return (List<TermPair<String,Integer>>)(List<?>)list;
191 								}
192 							}
193 							return null;
194 						}
195 						@Override
196 						protected Display getDisplay(Element element) {
197 							NodeData style = styleMap.get(element);
198 							return style.getProperty("display", false);
199 						}
200 						@Override
201 						protected Collection<Term<?>> getMarkerContent(Element element) {
202 							NodeData style = styleMap.get(element);
203 							for (PseudoElement pseudo : styleMap.pseudoSet(element)) {
204 								if ("marker".equals(pseudo.getName())) {
205 									NodeData pseudoStyle = styleMap.get(element, pseudo);
206 									if (pseudoStyle.getProperty("content", false) == Content.content_list) {
207 										List<Term<?>> content = pseudoStyle.getValue(TermList.class, "content", false);
208 										if (content != null)
209 											return content;
210 									}
211 									return Collections.emptyList();
212 								}
213 							}
214 							return null;
215 						}
216 						@Override
217 						protected CounterStyle getListStyleType(Element element, CounterStyle parentListStyleType) {
218 							NodeData style = styleMap.get(element);
219 							ListStyleType type = style.getProperty("list-style-type", false);
220 							if (type == null)
221 								return parentListStyleType; // value is inherited from parent
222 							else {
223 								switch (type) {
224 								case INHERIT:
225 									// should not happen
226 									return parentListStyleType;
227 								case INITIAL:
228 									return CounterStyle.DISC; // see https://www.w3.org/TR/css-lists-3/#propdef-list-style-type
229 								case counter_style_name:
230 									return getNamedCounterStyle(style.getValue(TermIdent.class, "list-style-type").getValue());
231 								case symbols_fn:
232 									return CounterStyle.fromSymbolsFunction(style.getValue(TermFunction.class, "list-style-type"));
233 								case braille_string: // note that braille CSS does not support any string, as standard CSS does
234 									return null;
235 								case DECIMAL:
236 									return CounterStyle.DECIMAL;
237 								case LOWER_ALPHA:
238 									return CounterStyle.LOWER_ALPHA;
239 								case LOWER_ROMAN:
240 									return CounterStyle.LOWER_ROMAN;
241 								case UPPER_ALPHA:
242 									return CounterStyle.UPPER_ALPHA;
243 								case UPPER_ROMAN:
244 									return CounterStyle.UPPER_ROMAN;
245 								case NONE:
246 								default:
247 									return null;
248 								}
249 							}
250 						}
251 						@Override
252 						protected CounterStyle getNamedCounterStyle(String name) {
253 							if (namedCounterStyles == null)
254 								namedCounterStyles = CounterStyle.parseCounterStyleRules(
255 									Iterables.filter(getParsedStyleSheet(), RuleCounterStyle.class));
256 							return namedCounterStyles.getOrDefault(name, CounterStyle.DECIMAL);
257 						}
258 					};
259 			XMLStreamWriterHelper.writeStartElement(writer, element);
260 			copyAttributes(element, writer);
261 			evaluator.startElement(element);
262 			String marker = evaluator.generateMarkerContents(element);
263 			if (marker != null) {
264 				if (markerContentAttributeName != null)
265 					// if attribute namespace specified, insert as "marker-content" attribute
266 					XMLStreamWriterHelper.writeAttribute(writer, markerContentAttributeName, marker);
267 				else if (markerAttributeName != null)
268 					// if single attribute specified, insert as ::marker {} pseudo-element rule
269 					XMLStreamWriterHelper.writeAttribute(
270 						writer, markerAttributeName,
271 						String.format("&::marker { content: '%s' }", marker.replaceAll("'", "\\\\'")));
272 				else
273 					// if no attribute specified, insert as text
274 					writer.writeCharacters(marker);
275 			}
276 			for (Node child = element.getFirstChild(); child != null; child = child.getNextSibling())
277 				traverse(child, styleMap, writer);
278 			evaluator.endElement();
279 			writer.writeEndElement();
280 		}
281 
282 		protected Map<QName,String> serializeStyle(NodeData mainStyle, Map<PseudoElement,NodeData> pseudoStyles, Element context) {
283 			throw new UnsupportedOperationException(); // not needed
284 		}
285 
286 		protected String serializeValue(Term<?> value) {
287 			return CssSerializer.toString(value);
288 		}
289 	}
290 
291 	private final static Logger logger = LoggerFactory.getLogger(DefaultCssCascader.class);
292 
293 }