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
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)
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
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 }