1   package org.daisy.pipeline.braille.dotify.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.Stack;
8   
9   import javax.xml.namespace.QName;
10  import static javax.xml.stream.XMLStreamConstants.END_ELEMENT;
11  import static javax.xml.stream.XMLStreamConstants.START_ELEMENT;
12  import javax.xml.stream.XMLStreamException;
13  import javax.xml.stream.XMLStreamWriter;
14  
15  import com.google.common.base.Splitter;
16  
17  import com.xmlcalabash.core.XProcRuntime;
18  import com.xmlcalabash.io.ReadablePipe;
19  import com.xmlcalabash.io.WritablePipe;
20  import com.xmlcalabash.library.DefaultStep;
21  import com.xmlcalabash.runtime.XAtomicStep;
22  
23  import net.sf.saxon.s9api.SaxonApiException;
24  
25  import org.daisy.common.stax.BaseURIAwareXMLStreamReader;
26  import org.daisy.common.stax.BaseURIAwareXMLStreamWriter;
27  import org.daisy.common.stax.XMLStreamWriterHelper.BufferedXMLStreamWriter;
28  import org.daisy.common.stax.XMLStreamWriterHelper.FutureWriterEvent;
29  import static org.daisy.common.stax.XMLStreamWriterHelper.writeAttribute;
30  import static org.daisy.common.stax.XMLStreamWriterHelper.writeAttributes;
31  import static org.daisy.common.stax.XMLStreamWriterHelper.writeEvent;
32  import static org.daisy.common.stax.XMLStreamWriterHelper.writeStartElement;
33  import org.daisy.common.transform.InputValue;
34  import org.daisy.common.transform.SingleInSingleOutXMLTransformer;
35  import org.daisy.common.transform.TransformerException;
36  import org.daisy.common.transform.XMLInputValue;
37  import org.daisy.common.transform.XMLOutputValue;
38  import org.daisy.common.xproc.calabash.XMLCalabashInputValue;
39  import org.daisy.common.xproc.calabash.XMLCalabashOutputValue;
40  import org.daisy.common.xproc.calabash.XProcStep;
41  import org.daisy.common.xproc.calabash.XProcStepProvider;
42  import org.daisy.common.xproc.XProcMonitor;
43  import static org.daisy.pipeline.braille.common.util.Strings.join;
44  
45  import org.osgi.service.component.annotations.Component;
46  
47  import org.slf4j.Logger;
48  import org.slf4j.LoggerFactory;
49  
50  public class ShiftObflMarkerStep extends DefaultStep implements XProcStep {
51  	
52  	@Component(
53  		name = "pxi:shift-obfl-marker",
54  		service = { XProcStepProvider.class },
55  		property = { "type:String={http://www.daisy.org/ns/pipeline/xproc/internal}shift-obfl-marker" }
56  	)
57  	public static class Provider implements XProcStepProvider {
58  		
59  		@Override
60  		public XProcStep newStep(XProcRuntime runtime, XAtomicStep step, XProcMonitor monitor, Map<String,String> properties) {
61  			return new ShiftObflMarkerStep(runtime, step);
62  		}
63  	}
64  	
65  	private ReadablePipe sourcePipe = null;
66  	private WritablePipe resultPipe = null;
67  	
68  	private static final String XMLNS_CSS = "http://www.daisy.org/ns/pipeline/braille-css";
69  	private static final QName CSS_OBFL_MARKER = new QName(XMLNS_CSS, "_obfl-marker");
70  	private static final QName CSS_BOX = new QName(XMLNS_CSS, "box");
71  	private static final QName CSS__ = new QName(XMLNS_CSS, "_");
72  	private static final QName _TYPE = new QName("type");
73  	
74  	private ShiftObflMarkerStep(XProcRuntime runtime, XAtomicStep step) {
75  		super(runtime, step);
76  	}
77  	
78  	@Override
79  	public void setInput(String port, ReadablePipe pipe) {
80  		sourcePipe = pipe;
81  	}
82  	
83  	@Override
84  	public void setOutput(String port, WritablePipe pipe) {
85  		resultPipe = pipe;
86  	}
87  	
88  	@Override
89  	public void reset() {
90  		sourcePipe.resetReader();
91  		resultPipe.resetWriter();
92  	}
93  	
94  	@Override
95  	public void run() throws SaxonApiException {
96  		super.run();
97  		try {
98  			new ShiftObflMarkerTransform()
99  			.transform(
100 				new XMLCalabashInputValue(sourcePipe),
101 				new XMLCalabashOutputValue(resultPipe, runtime))
102 			.run(); }
103 		catch (Throwable e) {
104 			throw XProcStep.raiseError(e, step); }
105 	}
106 	
107 	private static class ShiftObflMarkerTransform extends SingleInSingleOutXMLTransformer {
108 		
109 		public Runnable transform(XMLInputValue<?> source, XMLOutputValue<?> result, InputValue<?> params) throws IllegalArgumentException {
110 			if (source == null || result == null)
111 				throw new IllegalArgumentException();
112 			return () -> transform(source.ensureSingleItem().asXMLStreamReader(), result.asXMLStreamWriter());
113 		}
114 		
115 		void transform(BaseURIAwareXMLStreamReader reader, BaseURIAwareXMLStreamWriter output) throws TransformerException {
116 			BufferedXMLStreamWriter writer = new BufferedXMLStreamWriter(output);
117 			boolean insideInlineBox = false;
118 			Stack<Boolean> blockBoxes = new Stack<Boolean>();
119 			Stack<Boolean> inlineBoxes = new Stack<Boolean>();
120 			List<String> pendingMarker = new ArrayList<String>();
121 			ShiftedMarker shiftedMarker = null;
122 			try {
123 				int event = reader.getEventType();
124 				while (true)
125 					try {
126 						switch (event) {
127 						case START_ELEMENT: {
128 							writeEvent(writer, reader);
129 							boolean isInlineBox = false;
130 							boolean isBlockBox = false;
131 							if (insideInlineBox)
132 								writeAttributes(writer, reader);
133 							else {
134 								boolean isBox = CSS_BOX.equals(reader.getName());
135 								String marker = null;
136 								for (int i = 0; i < reader.getAttributeCount(); i++) {
137 									QName name = reader.getAttributeName(i);
138 									String value = reader.getAttributeValue(i);
139 									if (CSS_OBFL_MARKER.equals(name))
140 										marker = value;
141 									else {
142 										if (isBox && _TYPE.equals(name))
143 											if ("inline".equalsIgnoreCase(value))
144 												isInlineBox = true;
145 											else if ("block".equalsIgnoreCase(value))
146 												isBlockBox = true;
147 										writeAttribute(writer, name, value); }}
148 								if (isBlockBox || isInlineBox)
149 									if (shiftedMarker != null) {
150 										shiftedMarker.render();
151 										shiftedMarker = null; }
152 								if (isInlineBox) {
153 									if (marker != null)
154 										if (!pendingMarker.isEmpty())
155 											parseMarker(marker, pendingMarker);
156 										else
157 											writeAttribute(writer, CSS_OBFL_MARKER, marker);
158 									if (!pendingMarker.isEmpty()) {
159 										marker = serializeMarker(pendingMarker);
160 										pendingMarker.clear();
161 										if (marker != null)
162 											writeAttribute(writer, CSS_OBFL_MARKER, marker); }}
163 								else if (marker != null)
164 									parseMarker(marker, pendingMarker);
165 								if (isInlineBox)
166 									insideInlineBox = true; }
167 							blockBoxes.push(isBlockBox);
168 							inlineBoxes.push(isInlineBox);
169 							break; }
170 						case END_ELEMENT: {
171 							boolean isBlockBox = blockBoxes.pop();
172 							boolean isInlineBox = inlineBoxes.pop();
173 							if (isBlockBox) {
174 								if (!pendingMarker.isEmpty()) {
175 									if (shiftedMarker == null)
176 										throw new RuntimeException();
177 									else
178 										shiftedMarker.putAll(pendingMarker);
179 									pendingMarker.clear(); }}
180 							if (isInlineBox) {
181 								if (shiftedMarker != null)
182 									throw new RuntimeException("coding error");
183 								shiftedMarker = new ShiftedMarker();
184 								writer.writeEvent(shiftedMarker); }
185 							if (isInlineBox)
186 								insideInlineBox = false;
187 							writeEvent(writer, reader);
188 							break; }
189 						default:
190 							writeEvent(writer, reader); }
191 						event = reader.next(); }
192 					catch (NoSuchElementException e) {
193 						break; }
194 				if (!pendingMarker.isEmpty())
195 					if (shiftedMarker == null)
196 						throw new RuntimeException("invalid input");
197 					else
198 						shiftedMarker.putAll(pendingMarker);
199 				if (shiftedMarker != null)
200 					shiftedMarker.render();
201 				writer.flush(); }
202 			catch (XMLStreamException e) {
203 				throw new TransformerException(e); }
204 		}
205 		
206 		private static class ShiftedMarker implements FutureWriterEvent {
207 			
208 			private List<String> marker;
209 			private boolean ready = false;
210 			
211 			private void put(String marker) {
212 				if (this.marker == null)
213 					this.marker = new ArrayList<String>();
214 				this.marker.add(marker);
215 			}
216 			
217 			private void putAll(List<String> marker) {
218 				for (String m : marker) put(m);
219 			}
220 			
221 			private void render() {
222 				ready = true;
223 			}
224 			
225 			public void writeTo(XMLStreamWriter writer) throws XMLStreamException {
226 				if (!ready)
227 					throw new XMLStreamException("not ready");
228 				if (marker != null) {
229 					writeStartElement(writer, CSS__);
230 					writeAttribute(writer, CSS_OBFL_MARKER, serializeMarker(marker));
231 					writer.writeEndElement(); }
232 			}
233 			
234 			public boolean isReady() {
235 				return ready;
236 			}
237 		}
238 	}
239 	
240 	private static final Splitter markerSplitter = Splitter.on(' ').omitEmptyStrings();
241 	
242 	private static void parseMarker(String value, List<String> appendTo) {
243 		for (String m : markerSplitter.split(value))
244 			appendTo.add(m);
245 	}
246 	
247 	private static String serializeMarker(List<String> marker) {
248 		if (marker.isEmpty())
249 			return null;
250 		return join(marker, " ");
251 	}
252 	
253 	private static final Logger logger = LoggerFactory.getLogger(ShiftObflMarkerStep.class);
254 	
255 }