1 package org.daisy.pipeline.css.calabash.impl;
2
3 import java.net.URI;
4 import java.net.URL;
5 import java.util.ArrayList;
6 import java.util.Arrays;
7 import java.util.Collections;
8 import java.util.HashMap;
9 import java.util.Iterator;
10 import java.util.List;
11 import java.util.Map;
12
13 import javax.xml.transform.Source;
14 import javax.xml.transform.URIResolver;
15
16 import com.google.common.collect.ImmutableMap;
17 import static com.google.common.collect.Iterators.forArray;
18
19 import com.xmlcalabash.core.XProcException;
20 import com.xmlcalabash.core.XProcRuntime;
21 import com.xmlcalabash.io.ReadablePipe;
22 import com.xmlcalabash.io.WritablePipe;
23 import com.xmlcalabash.library.DefaultStep;
24 import com.xmlcalabash.model.RuntimeValue;
25 import com.xmlcalabash.runtime.XAtomicStep;
26
27 import net.sf.saxon.ma.map.MapItem;
28 import net.sf.saxon.s9api.QName;
29 import net.sf.saxon.s9api.SaxonApiException;
30
31 import org.daisy.common.saxon.SaxonBuffer;
32 import org.daisy.common.saxon.SaxonHelper;
33 import org.daisy.common.saxon.SaxonInputValue;
34 import org.daisy.common.transform.InputValue;
35 import org.daisy.common.transform.XMLInputValue;
36 import org.daisy.common.xproc.calabash.XMLCalabashInputValue;
37 import org.daisy.common.xproc.calabash.XMLCalabashOutputValue;
38 import org.daisy.common.xproc.calabash.XMLCalabashParameterInputValue;
39 import org.daisy.common.xproc.calabash.XProcStep;
40 import org.daisy.common.xproc.calabash.XProcStepProvider;
41 import org.daisy.common.xproc.XProcMonitor;
42 import org.daisy.pipeline.css.CssCascader;
43 import org.daisy.pipeline.css.Medium;
44 import org.daisy.pipeline.css.sass.SassCompiler;
45 import org.daisy.pipeline.css.UserAgentStylesheetRegistry;
46 import org.daisy.pipeline.css.XsltProcessor;
47
48 import org.osgi.service.component.annotations.Component;
49 import org.osgi.service.component.annotations.Reference;
50 import org.osgi.service.component.annotations.ReferenceCardinality;
51 import org.osgi.service.component.annotations.ReferencePolicy;
52
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 public class CssCascadeStep extends DefaultStep implements XProcStep {
57
58 private ReadablePipe sourcePipe = null;
59 private ReadablePipe contextPipe = null;
60 private WritablePipe resultPipe = null;
61 private final InMemoryURIResolver inMemoryResolver;
62 private final URIResolver cssURIResolver;
63 private final Iterable<CssCascader> inliners;
64 private final UserAgentStylesheetRegistry userAgentStylesheets;
65
66 private static final QName _user_stylesheet = new QName("user-stylesheet");
67 private static final QName _include_user_agent_stylesheet = new QName("include-user-agent-stylesheet");
68 private static final QName _parameters = new QName("parameters");
69 private static final QName _type = new QName("type");
70 private static final QName _content_type = new QName("content-type");
71 private static final QName _media = new QName("media");
72 private static final QName _attribute_name = new QName("attribute-name");
73 private static final QName _multiple_attributes = new QName("multiple-attributes");
74
75 private static final String DEFAULT_MEDIUM = "embossed";
76 private static final String DEFAULT_TYPES = "text/css text/x-scss";
77 private static final QName DEFAULT_ATTRIBUTE_NAME = new QName("style");
78
79 private CssCascadeStep(XProcRuntime runtime, XAtomicStep step,
80 Iterable<CssCascader> inliners, final URIResolver resolver,
81 UserAgentStylesheetRegistry userAgentStylesheets) {
82 super(runtime, step);
83 this.inliners = inliners;
84 this.userAgentStylesheets = userAgentStylesheets;
85
86 inMemoryResolver = new InMemoryURIResolver();
87
88
89 cssURIResolver = fallback(inMemoryResolver, resolver);
90 }
91
92 @Override
93 public void setInput(String port, ReadablePipe pipe) {
94 if ("source".equals(port))
95 sourcePipe = pipe;
96 else
97 contextPipe = pipe;
98 }
99
100 @Override
101 public void setOutput(String port, WritablePipe pipe) {
102 resultPipe = pipe;
103 }
104
105 @Override
106 public void reset() {
107 sourcePipe.resetReader();
108 contextPipe.resetReader();
109 resultPipe.resetWriter();
110 }
111
112 @Override
113 public void run() throws SaxonApiException {
114 super.run();
115 try {
116 Medium medium; {
117 try {
118 medium = Medium.parse(getOption(_media, DEFAULT_MEDIUM));
119 } catch (Throwable e) {
120 throw new IllegalArgumentException("Could not parse media: " + getOption(_media, DEFAULT_MEDIUM), e);
121 }
122 }
123 List<String> types = Arrays.asList(getOption(_type, DEFAULT_TYPES).trim().split("\\s+"));
124 if (!types.contains("text/css"))
125 throw new XProcException(step, "'type' option must contain 'text/css'");
126 boolean enableSass = types.contains("text/x-scss");
127 List<String> stylesheets = new ArrayList<>();
128 if (getOption(_include_user_agent_stylesheet, false))
129 for (URL u : userAgentStylesheets.get(types,
130 Arrays.asList(getOption(_content_type, "").trim().split("\\s+")),
131 Collections.singleton(medium)))
132 stylesheets.add(u.toString());
133 for (String s : Arrays.asList(getOption(_user_stylesheet, "").trim().split("\\s+")))
134 stylesheets.add(s);
135 Map<String,String> sassVariables = new HashMap<>();
136 RuntimeValue paramOption = getOption(_parameters);
137 if (paramOption != null)
138 for (Map.Entry<String,Object> e
139 : SaxonHelper.mapFromMapItem(
140 (MapItem)SaxonHelper.getSingleItem(paramOption.getValue().getUnderlyingValue()),
141 Object.class
142 ).entrySet())
143 sassVariables.put(e.getKey(), "" + e.getValue());
144 for (CssCascader inliner : inliners)
145 if (inliner.supportsMedium(medium)) {
146 QName attributeName; {
147 RuntimeValue v = getOption(_attribute_name);
148 if (v == null)
149 attributeName = DEFAULT_ATTRIBUTE_NAME;
150 else if (v.getValue().size() == 0)
151 attributeName = null;
152 else
153 attributeName = v.getQName(); }
154 boolean multipleAttrs = getOption(_multiple_attributes, false);
155 if (multipleAttrs && (attributeName == null
156 || attributeName.getNamespaceURI() == null
157 || "".equals(attributeName.getNamespaceURI())))
158 throw new IllegalArgumentException(
159 "Namespace must be specified when cascading to multiple attributes per element");
160 inMemoryResolver.setContext(contextPipe);
161 inliner.newInstance(
162 medium,
163 String.join(" ", stylesheets),
164 cssURIResolver,
165 enableSass
166 ? new SassCompiler(cssURIResolver, Collections.unmodifiableMap(sassVariables))
167 : null,
168 new XSLT(runtime, step),
169 attributeName != null ? SaxonHelper.jaxpQName(attributeName) : null,
170 multipleAttrs
171 ).transform(
172 new XMLCalabashInputValue(sourcePipe),
173 new XMLCalabashOutputValue(resultPipe, runtime)
174 ).run();
175 return; }
176 throw new XProcException(step, "No CSS inliner implementation found for medium " + medium); }
177 catch (Throwable e) {
178 throw XProcStep.raiseError(e, step); }
179 }
180
181 private static URIResolver fallback(final URIResolver... resolvers) {
182 return new URIResolver() {
183 public Source resolve(String href, String base) throws javax.xml.transform.TransformerException {
184 Iterator<URIResolver> iterator = forArray(resolvers);
185 while (iterator.hasNext()) {
186 Source source = iterator.next().resolve(href, base);
187 if (source != null)
188 return source; }
189 return null;
190 }
191 };
192 }
193
194
195 private class XSLT extends com.xmlcalabash.library.XSLT implements XProcStep, XsltProcessor {
196 public XSLT(XProcRuntime runtime, XAtomicStep step) {
197 super(runtime, step);
198 }
199 public void setParameter(String port, QName name, RuntimeValue value) {
200 if ("parameters".equals(port))
201 setParameter(name, value);
202 else
203 super.setParameter(port, name, value);
204 }
205 public XMLInputValue<Void> transform(URI stylesheetURI,
206 XMLInputValue<?> source,
207 Map<javax.xml.namespace.QName,InputValue<?>> parameters) {
208 XMLInputValue<Void> stylesheet = new SaxonInputValue(runtime.parse(stylesheetURI.toASCIIString(), null));
209 SaxonBuffer buf = new SaxonBuffer(runtime.getProcessor().getUnderlyingConfiguration());
210 transform(
211 ImmutableMap.of(
212 new javax.xml.namespace.QName("source"), source,
213 new javax.xml.namespace.QName("stylesheet"), stylesheet,
214 new javax.xml.namespace.QName("parameters"), XMLCalabashParameterInputValue.of(new InputValue<>(parameters))),
215 ImmutableMap.of(
216 new javax.xml.namespace.QName("result"), buf.asOutput())
217 ).run();
218 buf.done();
219 return buf.asInput();
220 }
221 }
222
223 @Component(
224 name = "pxi:css-cascade",
225 service = { XProcStepProvider.class },
226 property = { "type:String={http://www.daisy.org/ns/pipeline/xproc/internal}css-cascade" }
227 )
228 public static class Provider implements XProcStepProvider {
229
230 private URIResolver resolver;
231 private UserAgentStylesheetRegistry userAgentStylesheets;
232 private final List<CssCascader> inliners = new ArrayList<>();
233
234 @Override
235 public XProcStep newStep(XProcRuntime runtime, XAtomicStep step, XProcMonitor monitor, Map<String,String> properties) {
236 return new CssCascadeStep(runtime, step, inliners, resolver, userAgentStylesheets);
237 }
238
239 @Reference(
240 name = "URIResolver",
241 unbind = "-",
242 service = URIResolver.class,
243 cardinality = ReferenceCardinality.MANDATORY,
244 policy = ReferencePolicy.STATIC
245 )
246 public void setUriResolver(URIResolver resolver) {
247 this.resolver = resolver;
248 }
249
250 @Reference(
251 name = "CssCascader",
252 unbind = "-",
253 service = CssCascader.class,
254 cardinality = ReferenceCardinality.MULTIPLE,
255 policy = ReferencePolicy.STATIC
256 )
257 public void bindCssCascader(CssCascader inliner) {
258 this.inliners.add(inliner);
259 }
260
261 @Reference(
262 name = "UserAgentStylesheetRegistry",
263 unbind = "-",
264 service = UserAgentStylesheetRegistry.class,
265 cardinality = ReferenceCardinality.MANDATORY,
266 policy = ReferencePolicy.STATIC
267 )
268 public void setUserAgentStylesheetRegistry(UserAgentStylesheetRegistry registry) {
269 this.userAgentStylesheets = registry;
270 }
271 }
272
273 private static final Logger logger = LoggerFactory.getLogger(CssCascadeStep.class);
274
275 }