1 package org.daisy.pipeline.braille.css.calabash.impl;
2
3 import java.util.ArrayList;
4 import java.util.List;
5 import java.util.Map;
6 import java.util.NoSuchElementException;
7 import java.util.Set;
8 import java.util.Stack;
9 import java.util.TreeMap;
10 import java.util.TreeSet;
11
12 import javax.xml.namespace.QName;
13 import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
14 import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;
15 import javax.xml.stream.XMLStreamException;
16 import javax.xml.stream.XMLStreamWriter;
17
18 import com.xmlcalabash.core.XProcRuntime;
19 import com.xmlcalabash.io.ReadablePipe;
20 import com.xmlcalabash.io.WritablePipe;
21 import com.xmlcalabash.library.DefaultStep;
22 import com.xmlcalabash.runtime.XAtomicStep;
23
24 import net.sf.saxon.s9api.SaxonApiException;
25
26 import org.daisy.common.saxon.SaxonBuffer;
27 import org.daisy.common.stax.BaseURIAwareXMLStreamReader;
28 import org.daisy.common.stax.BaseURIAwareXMLStreamWriter;
29 import org.daisy.common.stax.XMLStreamWriterHelper.BufferedXMLStreamWriter;
30 import org.daisy.common.stax.XMLStreamWriterHelper.FutureWriterEvent;
31 import static org.daisy.common.stax.XMLStreamWriterHelper.writeAttribute;
32 import static org.daisy.common.stax.XMLStreamWriterHelper.writeAttributes;
33 import static org.daisy.common.stax.XMLStreamWriterHelper.writeEvent;
34 import static org.daisy.common.stax.XMLStreamWriterHelper.writeStartElement;
35 import org.daisy.common.transform.InputValue;
36 import org.daisy.common.transform.SingleInSingleOutXMLTransformer;
37 import org.daisy.common.transform.TransformerException;
38 import org.daisy.common.transform.XMLInputValue;
39 import org.daisy.common.transform.XMLOutputValue;
40 import org.daisy.common.xproc.calabash.XMLCalabashInputValue;
41 import org.daisy.common.xproc.calabash.XMLCalabashOutputValue;
42 import org.daisy.common.xproc.calabash.XProcStep;
43 import org.daisy.common.xproc.calabash.XProcStepProvider;
44 import org.daisy.common.xproc.XProcMonitor;
45
46 import org.osgi.service.component.annotations.Component;
47
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
50
51 public class CssShiftIdStep extends DefaultStep implements XProcStep {
52
53 @Component(
54 name = "css:shift-id",
55 service = { XProcStepProvider.class },
56 property = { "type:String={http://www.daisy.org/ns/pipeline/braille-css}shift-id" }
57 )
58 public static class Provider implements XProcStepProvider {
59
60 @Override
61 public XProcStep newStep(XProcRuntime runtime, XAtomicStep step, XProcMonitor monitor, Map<String,String> properties) {
62 return new CssShiftIdStep(runtime, step);
63 }
64 }
65
66 private ReadablePipe sourcePipe = null;
67 private WritablePipe resultPipe = null;
68
69 private static final String XMLNS_CSS = "http://www.daisy.org/ns/pipeline/braille-css";
70 private static final QName CSS_ID = new QName(XMLNS_CSS, "id");
71 private static final QName CSS_BOX = new QName(XMLNS_CSS, "box");
72 private static final QName CSS__ = new QName(XMLNS_CSS, "_");
73 private static final QName _TYPE = new QName("type");
74 private static final QName CSS_COUNTER = new QName(XMLNS_CSS, "counter");
75 private static final QName _NAME = new QName("name");
76 private static final QName _TARGET = new QName("target");
77 private static final QName CSS_ANCHOR = new QName(XMLNS_CSS, "anchor");
78 private static final QName CSS_FLOW = new QName(XMLNS_CSS, "flow");
79
80 private CssShiftIdStep(XProcRuntime runtime, XAtomicStep step) {
81 super(runtime, step);
82 }
83
84 @Override
85 public void setInput(String port, ReadablePipe pipe) {
86 sourcePipe = pipe;
87 }
88
89 @Override
90 public void setOutput(String port, WritablePipe pipe) {
91 resultPipe = pipe;
92 }
93
94 @Override
95 public void reset() {
96 sourcePipe.resetReader();
97 resultPipe.resetWriter();
98 }
99
100 @Override
101 public void run() throws SaxonApiException {
102 super.run();
103 try {
104
105
106 Map<String,String> idMap = new TreeMap<String,String>();
107 Set<String> discardedIds = new TreeSet<String>();
108 new ShiftIdTransformer(idMap, discardedIds)
109 .andThen(new UpdateRefsTransformer(idMap, discardedIds),
110 new SaxonBuffer(runtime.getProcessor().getUnderlyingConfiguration()),
111 false)
112 .transform(
113 new XMLCalabashInputValue(sourcePipe),
114 new XMLCalabashOutputValue(resultPipe, runtime))
115 .run(); }
116 catch (Throwable e) {
117 throw XProcStep.raiseError(e, step); }
118 }
119
120 private static class ShiftIdTransformer extends SingleInSingleOutXMLTransformer {
121
122 private final Map<String,String> idMap;
123 private final Set<String> discardedIds;
124
125 public ShiftIdTransformer(Map<String,String> idMap, Set<String> discardedIds) {
126 this.idMap = idMap;
127 this.discardedIds = discardedIds;
128 }
129
130 public Runnable transform(XMLInputValue<?> source, XMLOutputValue<?> result, InputValue<?> params) throws IllegalArgumentException {
131 if (source == null || result == null)
132 throw new IllegalArgumentException();
133 return () -> transform(source.asXMLStreamReader(), result.asXMLStreamWriter());
134 }
135
136 public void transform(BaseURIAwareXMLStreamReader reader, BaseURIAwareXMLStreamWriter output) throws TransformerException {
137 BufferedXMLStreamWriter writer = new BufferedXMLStreamWriter(output);
138 boolean insideInlineBox = false;
139 Stack<Boolean> blockBoxes = new Stack<Boolean>();
140 Stack<Boolean> inlineBoxes = new Stack<Boolean>();
141 List<String> pendingId = new ArrayList<String>();
142 ShiftedId shiftedId = null;
143 int depth = 0;
144 String currentFlow = "normal";
145 try {
146 int event = reader.getEventType();
147 while (true)
148 try {
149 switch (event) {
150 case START_ELEMENT: {
151 writeEvent(writer, reader);
152 boolean isInlineBox = false;
153 boolean isBlockBox = false;
154 if (insideInlineBox)
155 writeAttributes(writer, reader);
156 else {
157 boolean isBox = CSS_BOX.equals(reader.getName());
158 String id = null;
159 String flow = depth == 0 ? "normal" : currentFlow;
160 for (int i = 0; i < reader.getAttributeCount(); i++) {
161 QName name = reader.getAttributeName(i);
162 String value = reader.getAttributeValue(i);
163 if (CSS_ID.equals(name))
164 id = value;
165 else {
166 if (isBox && _TYPE.equals(name))
167 if ("inline".equalsIgnoreCase(value))
168 isInlineBox = true;
169 else if ("block".equalsIgnoreCase(value))
170 isBlockBox = true;
171 if (depth == 0 && CSS_FLOW.equals(name))
172 flow = value;
173 writeAttribute(writer, name, value); }}
174 boolean newFlow = !flow.equals(currentFlow);
175 currentFlow = flow;
176 if (newFlow || isBlockBox) {
177 if (!pendingId.isEmpty()) {
178 if (shiftedId == null) {
179 if (newFlow) {
180 discardedIds.addAll(pendingId);
181 pendingId.clear(); }}
182 else {
183 shiftedId.putAll(pendingId);
184 pendingId.clear(); }}
185 if (shiftedId != null) {
186 shiftedId.render();
187 shiftedId = null; }}
188 if (isInlineBox && shiftedId != null) {
189 shiftedId.render();
190 shiftedId = null; }
191 if (isInlineBox) {
192 if (id != null)
193 if (!pendingId.isEmpty())
194 pendingId.add(id);
195 else
196 writeAttribute(writer, CSS_ID, id);
197 if (!pendingId.isEmpty()) {
198 id = pendingId.get(pendingId.size() - 1);
199 for (String oldId : pendingId)
200 if (!oldId.equals(id))
201 idMap.put(oldId, id);
202 pendingId.clear();
203 if (id != null)
204 writeAttribute(writer, CSS_ID, id); }}
205 else if (id != null)
206 pendingId.add(id);
207 if (isInlineBox)
208 insideInlineBox = true; }
209 blockBoxes.push(isBlockBox);
210 inlineBoxes.push(isInlineBox);
211 depth++;
212 break; }
213 case END_ELEMENT: {
214 depth--;
215 boolean isBlockBox = blockBoxes.pop();
216 boolean isInlineBox = inlineBoxes.pop();
217 if (isBlockBox) {
218 if (!pendingId.isEmpty()) {
219 if (shiftedId == null)
220 discardedIds.addAll(pendingId);
221 else
222 shiftedId.putAll(pendingId);
223 pendingId.clear(); }}
224 else if (isInlineBox) {
225 if (shiftedId != null)
226 throw new RuntimeException("coding error");
227 shiftedId = new ShiftedId();
228 writer.writeEvent(shiftedId);
229 insideInlineBox = false; }
230 writeEvent(writer, reader);
231 break; }
232 default:
233 writeEvent(writer, reader); }
234 event = reader.next(); }
235 catch (NoSuchElementException e) {
236 break; }
237 if (!pendingId.isEmpty())
238 if (shiftedId == null)
239 throw new RuntimeException("invalid input");
240 else
241 shiftedId.putAll(pendingId);
242 if (shiftedId != null)
243 shiftedId.render();
244 writer.flush(); }
245 catch (XMLStreamException e) {
246 throw new TransformerException(e); }
247 }
248
249 private class ShiftedId implements FutureWriterEvent {
250
251 private List<String> ids;
252 private boolean ready = false;
253
254 private void put(String id) {
255 if (this.ids == null)
256 this.ids = new ArrayList<String>();
257 this.ids.add(id);
258 }
259
260 private void putAll(List<String> ids) {
261 for (String id : ids) put(id);
262 }
263
264 private void render() {
265 ready = true;
266 }
267
268 public void writeTo(XMLStreamWriter writer) throws XMLStreamException {
269 if (!ready)
270 throw new XMLStreamException("not ready");
271 if (ids != null) {
272 String id = ids.get(0);
273 for (String oldId : ids)
274 if (!oldId.equals(id))
275 idMap.put(oldId, id);
276 writeStartElement(writer, CSS__);
277 writeAttribute(writer, CSS_ID, id);
278 writer.writeEndElement(); }
279 }
280
281 public boolean isReady() {
282 return ready;
283 }
284 }
285 }
286
287 private static class UpdateRefsTransformer extends SingleInSingleOutXMLTransformer {
288
289 private final Map<String,String> idMap;
290 private final Set<String> discardedIds;
291
292 public UpdateRefsTransformer(Map<String,String> idMap, Set<String> discardedIds) {
293 this.idMap = idMap;
294 this.discardedIds = discardedIds;
295 }
296
297 public Runnable transform(XMLInputValue<?> source, XMLOutputValue<?> result, InputValue<?> params) throws IllegalArgumentException {
298 if (source == null || result == null)
299 throw new IllegalArgumentException();
300 return () -> transform(source.asXMLStreamReader(), result.asXMLStreamWriter());
301 }
302
303 void transform(BaseURIAwareXMLStreamReader reader, BaseURIAwareXMLStreamWriter output) throws TransformerException {
304 BufferedXMLStreamWriter writer = new BufferedXMLStreamWriter(output);
305 try {
306 while (true)
307 try {
308 int event = reader.getEventType();
309 switch (event) {
310 case START_ELEMENT: {
311 writeEvent(writer, reader);
312 QName elemName = reader.getName();
313 boolean isCounter = CSS_COUNTER.equals(elemName);
314 String counterTarget = null;
315 String anchor = null;
316 for (int i = 0; i < reader.getAttributeCount(); i++) {
317 QName attrName = reader.getAttributeName(i);
318 String attrValue = reader.getAttributeValue(i);
319 if (CSS_ANCHOR.equals(attrName))
320 anchor = attrValue;
321 else if (isCounter && _TARGET.equals(attrName)) {
322 counterTarget = attrValue;
323 } else {
324 writeAttribute(writer, attrName, attrValue);
325 }
326 }
327 if (anchor != null) {
328 if (idMap.containsKey(anchor)) {
329 anchor = idMap.get(anchor);
330 } else if (discardedIds.contains(anchor)) {
331 throw new RuntimeException();
332 }
333 writeAttribute(writer, CSS_ANCHOR, anchor);
334 }
335 if (counterTarget != null) {
336 if (idMap.containsKey(counterTarget)) {
337 counterTarget = idMap.get(counterTarget);
338 } else if (discardedIds.contains(counterTarget)) {
339 throw new RuntimeException();
340 }
341 writeAttribute(writer, _TARGET, counterTarget);
342 }
343 break; }
344 default:
345 writeEvent(writer, reader); }
346 reader.next(); }
347 catch (NoSuchElementException e) {
348 break; }
349 writer.flush(); }
350 catch (XMLStreamException e) {
351 throw new TransformerException(e); }
352 }
353 }
354
355 private static final Logger logger = LoggerFactory.getLogger(CssShiftIdStep.class);
356
357 }