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  		// URI resolver that can resolve in-memory documents
86  		inMemoryResolver = new InMemoryURIResolver();
87  		// URI resolver for CSS files
88  		// first check memory and fall back to the resolver from module-registry
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 	// extend XSLT to make it implement XMLTransformer
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 }