1   package org.daisy.pipeline.css.speech.impl;
2   
3   import java.net.URI;
4   import java.util.Arrays;
5   import java.util.HashMap;
6   import java.util.HashSet;
7   import java.util.Iterator;
8   import java.util.List;
9   import java.util.Map;
10  import java.util.Set;
11  
12  import javax.xml.namespace.QName;
13  import javax.xml.transform.URIResolver;
14  
15  import cz.vutbr.web.css.CSSProperty;
16  import cz.vutbr.web.css.CSSProperty.Content;
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.SupportedCSS;
21  import cz.vutbr.web.css.Term;
22  import cz.vutbr.web.css.TermFunction;
23  import cz.vutbr.web.css.TermIdent;
24  import cz.vutbr.web.css.TermList;
25  import cz.vutbr.web.css.TermString;
26  import cz.vutbr.web.css.TermURI;
27  import cz.vutbr.web.csskit.antlr.CSSParserFactory;
28  import cz.vutbr.web.csskit.RuleFactoryImpl;
29  import cz.vutbr.web.domassign.DeclarationTransformer;
30  import cz.vutbr.web.domassign.SupportedCSS21;
31  
32  import org.daisy.braille.css.BrailleCSSParserFactory;
33  import org.daisy.common.file.URLs;
34  import org.daisy.common.transform.XMLTransformer;
35  import org.daisy.pipeline.css.CssCascader;
36  import org.daisy.pipeline.css.CssPreProcessor;
37  import org.daisy.pipeline.css.CssSerializer;
38  import org.daisy.pipeline.css.JStyleParserCssCascader;
39  import org.daisy.pipeline.css.Medium;
40  import org.daisy.pipeline.css.speech.SpeechDeclarationTransformer;
41  import org.daisy.pipeline.css.XsltProcessor;
42  
43  import org.osgi.service.component.annotations.Component;
44  
45  import org.slf4j.Logger;
46  import org.slf4j.LoggerFactory;
47  
48  import org.w3c.dom.Element;
49  
50  @Component(
51  	name = "SpeechCssCascader",
52  	service = { CssCascader.class }
53  )
54  public class SpeechCssCascader implements CssCascader {
55  
56  	public boolean supportsMedium(Medium medium) {
57  		switch (medium.getType()) {
58  		case SPEECH:
59  			return true;
60  		default:
61  			return false;
62  		}
63  	}
64  
65  	public XMLTransformer newInstance(Medium medium,
66  	                                  String userAndUserAgentStylesheets,
67  	                                  URIResolver uriResolver,
68  	                                  CssPreProcessor preProcessor,
69  	                                  XsltProcessor xsltProcessor,
70  	                                  QName attributeName,
71  	                                  boolean multipleAttrs) {
72  		if (!multipleAttrs)
73  			throw new UnsupportedOperationException("Cascading to single attribute per element not supported");
74  		switch (medium.getType()) {
75  		case SPEECH:
76  			return new Transformer(uriResolver, preProcessor, xsltProcessor, userAndUserAgentStylesheets, medium, attributeName);
77  		default:
78  			throw new IllegalArgumentException("medium not supported: " + medium);
79  		}
80  	}
81  
82  	// using braille-css because :has() and :note() are not supported by jStyleParser
83  	private static final CSSParserFactory parserFactory = new BrailleCSSParserFactory();
84  	private static final RuleFactory ruleFactory = RuleFactoryImpl.getInstance();
85  	private static final SupportedCSS speechCSS = SupportedCSS21.getInstance();
86  	private static final DeclarationTransformer declarationTransformer = new SpeechDeclarationTransformer();
87  	private static final Set<String> speechCSSProperties = new HashSet<>(
88  		Arrays.asList(
89  			"voice-family", "stress", "richness", "cue", "cue-before", "cue-after", "pause",
90  			"pause-after", "pause-before", "azimuth", "volume", "speak", "play-during",
91  			"elevation", "speech-rate", "pitch", "pitch-range", "stress", "speak-punctuation",
92  			"speak-numeral", "speak-header"));
93  
94  	private static class Transformer extends JStyleParserCssCascader {
95  
96  		private final String attributePrefix;
97  		private final String attributeNamespaceURI;
98  
99  		private Transformer(URIResolver resolver, CssPreProcessor preProcessor, XsltProcessor xsltProcessor,
100 		                    String userAndUserAgentStylesheets, Medium medium, QName attributeNamespace) {
101 			super(resolver, preProcessor, xsltProcessor, userAndUserAgentStylesheets, medium, null,
102 			      parserFactory, ruleFactory, speechCSS, declarationTransformer);
103 			this.attributePrefix = attributeNamespace.getPrefix();
104 			this.attributeNamespaceURI = attributeNamespace.getNamespaceURI();
105 		}
106 
107 		protected Map<QName,String> serializeStyle(NodeData mainStyle, Map<PseudoElement,NodeData> pseudoStyles, Element context) {
108 			Map<QName,String> style = null;
109 			if (mainStyle != null) {
110 				for (String property : mainStyle.getPropertyNames()) {
111 					if (speechCSSProperties.contains(property)) {
112 						String s; {
113 							Term<?> v = mainStyle.getValue(property, false);
114 							if (v == null || v.getValue() == null) {
115 								CSSProperty prop = mainStyle.getProperty(property, false);
116 								if (prop == null) // can be null for unspecified inherited properties
117 									continue;
118 								s = prop.toString().replace('_', '-');
119 							} else {
120 								s = serializeTerm(v);
121 							}
122 						}
123 						if (style == null)
124 							style = new HashMap<>();
125 						style.put(new QName(attributeNamespaceURI, property, attributePrefix), s);
126 					}
127 				}
128 			}
129 			// process ::after and ::before pseudo elements
130 			for (PseudoElement pseudo : pseudoStyles.keySet()) {
131 				if ("::after".equals(pseudo.toString()) || "::before".equals(pseudo.toString())) {
132 					NodeData nd = pseudoStyles.get(pseudo);
133 					if (nd != null) {
134 						for (String property : nd.getPropertyNames()) {
135 							if ("content".equals(property)) {
136 								switch ((Content)nd.getProperty(property)) {
137 								case list_values:
138 									StringBuilder value = new StringBuilder();
139 									for (Term<?> t : (TermList)nd.getValue(property, false)) {
140 										if (t instanceof TermString) {
141 											value.append(t.getValue());
142 											continue;
143 										} else if (t instanceof TermFunction) {
144 											TermFunction f = (TermFunction)t;
145 											if ("attr".equals(f.getFunctionName().toLowerCase())
146 											    && f.size() == 1
147 											    && f.get(0) instanceof TermIdent) {
148 												value.append(context.getAttribute(((TermIdent)f.get(0)).getValue()));
149 												continue;
150 											}
151 										}
152 										logger.warn("Don't know how to speak content value " + t
153 										            + " within ::" + pseudo.getName() + " pseudo-element");
154 										value = null;
155 										break;
156 									}
157 									if (value != null && value.length() > 0) {
158 										if (style == null)
159 											style = new HashMap<>();
160 										style.put(new QName(attributeNamespaceURI, pseudo.getName(), attributePrefix), value.toString());
161 									}
162 									break;
163 								case NORMAL:
164 								case NONE:
165 								case INHERIT:
166 								case INITIAL:
167 								default:
168 									break;
169 								}
170 								break;
171 							} else if (speechCSSProperties.contains(property)) {
172 								logger.warn("Ignoring property '"+ property
173 								            + "' within ::" + pseudo.getName() + " pseudo-element");
174 							}
175 						}
176 					}
177 				}
178 			}
179 			return style;
180 		}
181 
182 		private static String serializeTerm(Term<?> term) {
183 			if (term instanceof TermString)
184 				return ((TermString)term).getValue();
185 			else if (term instanceof TermURI) {
186 				TermURI termURI = (TermURI)term;
187 				URI uri = URLs.asURI(termURI.getValue());
188 				if (termURI.getBase() != null)
189 					uri = URLs.resolve(URLs.asURI(termURI.getBase()), uri);
190 				return uri.toASCIIString();
191 			} else if (term instanceof TermIdent)
192 				return CssSerializer.toString(term).replace('_', '-');
193 			else
194 				return CssSerializer.toString(term, Transformer::serializeTerm);
195 		}
196 
197 		protected String serializeValue(Term<?> value) {
198 			throw new UnsupportedOperationException();
199 		}
200 	}
201 
202 	private final static Logger logger = LoggerFactory.getLogger(SpeechCssCascader.class);
203 
204 }