1 /*
2  * Copyright (C) 2015-2019, by Laszlo Szeremi under the Boost license.
3  *
4  * Pixel Perfect Engine, concrete.text module
5  */
6 
7 module PixelPerfectEngine.concrete.text;
8 
9 public import PixelPerfectEngine.graphics.fontsets;
10 public import PixelPerfectEngine.graphics.bitmap;
11 
12 //import std.xml;
13 import std.experimental.xml.sax;
14 import dom = std.experimental.xml.dom;
15 import std.experimental.xml.lexers;
16 import std.experimental.xml.parser;
17 
18 /**
19  * Implements a formatted text chunk, that can be serialized in XML form.
20  * Has a linked list structure to easily link multiple chunks after each other.
21  */
22 public class Text(BitmapType = Bitmap8Bit) {
23 
24 	public dstring			text;			///The text to be displayed
25 	public CharacterFormattingInfo!BitmapType 	formatting;	///The formatting of this text block
26 	public Text				next;			///The next piece of formatted text block
27 	public int				frontTab;		///Space before the text chunk in pixels. Can be negative.
28 	public BitmapType		icon;			///Icon inserted in front of the text chunk.
29 	public byte				iconOffsetX;	///X offset of the icon if any
30 	public byte				iconOffsetY;	///Y offset of the icon if any
31 	/**
32 	 * Creates a unit of formatted text from the supplied data.
33 	 */
34 	public this(dstring text, CharacterFormattingInfo!BitmapType formatting, Text next = null, int frontTab = 0,
35 			BitmapType icon = null) @safe pure @nogc nothrow {
36 		this.text = text;
37 		this.formatting = formatting;
38 		this.next = next;
39 		this.frontTab = frontTab;
40 		this.icon = icon;
41 	}
42 	/**
43 	 * Parses formatted text from binary.
44 	 * Header is the first 4 bytes of the stream.
45 	 */
46 	public this(ubyte[] stream, Fontset!BitmapType[uint] fonts) @safe pure {
47 		import PixelPerfectEngine.system.etc : reinterpretCast, reinterpretGet;
48 		assert (stream.length > 4, "Bytestream too short!");
49 		const uint header = reinterpretGet!uint(stream[0..4]);
50 		stream = stream[4..$];
51 		switch (header & 0xF) {
52 			case TextType.UTF8:
53 				__ctor(TextType.UTF8, stream, fonts);
54 				break;
55 			case TextType.UTF32:
56 				__ctor(TextType.UTF32, stream, fonts);
57 				break;
58 			default:
59 				throw new Exception ("Binary character formatting stream error!");
60 		}
61 	}
62 	private this(TextType type, ubyte[] stream, Fontset!BitmapType[uint] fonts) @safe pure {
63 		import PixelPerfectEngine.system.etc : reinterpretCast, reinterpretGet;
64 		import std.utf : toUTF32;
65 		//uint header = reinterpretGet!uint(stream[0..4]);
66 		//stream = stream[4..$];
67 		if (stream[0] == FormattingCharacters.binaryCFI) {
68 			stream = stream[1..$];
69 			Fontset!BitmapType fontType = fonts[reinterpretGet!uint(stream[0..4])];
70 			const ubyte color = stream[7];
71 			const uint formatFlags = reinterpretGet!uint(stream[8..12]);
72 			const ushort paragraphSpace = reinterpretGet!ushort(stream[12..14]);
73 			const short rowHeight = reinterpretGet!short(stream[14..16]);
74 			formatting = new CharacterFormattingInfo!BitmapType(fontType, color, formatFlags, paragraphSpace, rowHeight);
75 			stream = stream[16..$];
76 		} else throw new Exception("Binary character formatting stream error!");
77 		if (stream[0] == FormattingCharacters.binaryLI) {
78 			stream = stream[1..$];
79 			const uint charStrmL = reinterpretGet!uint(stream[0..4]);
80 			if (charStrmL <= stream.length) {
81 				stream = stream[4..$];
82 				if(type == TextType.UTF8)
83 					text = toUTF32(reinterpretCast!char(stream[0..charStrmL]));
84 				else if(type == TextType.UTF32)
85 					text = reinterpretCast!dchar(stream[0..charStrmL]);
86 				stream = stream[charStrmL..$];
87 			} else throw new Exception("Binary character formatting stream error!");
88 			if (stream.length) {
89 				next = new Text!BitmapType(type, stream, fonts);
90 			}
91 		}
92 	}
93 	/**
94 	 * Returns the text as a 32bit string without the formatting.
95 	 */
96 	public dstring toDString() @safe pure nothrow {
97 		if (next)
98 			return text ~ next.toDString;
99 		else
100 			return text;
101 	}
102 }
103 
104 /**
105  * Parses text from XML/ETML
106  *
107  * See "ETML.md" for info.
108  */
109 public class TextParser(BitmapType = Bitmap8Bit, StringType = string)
110 		/+if((typeof(BitmapType) is Bitmap8Bit || typeof(BitmapType) is Bitmap16Bit ||
111 		typeof(BitmapType) is Bitmap32Bit) && (typeof(StringType) is string || typeof(StringType) is dstring))+/ {
112 	private Text!BitmapType	 		current;	///Current element
113 	//private Text!BitmapType			_output;	///Root/output element
114 	private Text!BitmapType[] 		stack;		///Mostly to refer back to previous elements
115 	private CharacterFormattingInfo!BitmapType	currFrmt;///currently parsed formatting
116 	public CharacterFormattingInfo!BitmapType[]	chrFrmt;///All character format, that has been parsed so far
117 	private CharacterFormattingInfo!BitmapType	defFrmt;///Default character formatting. Must be set before parsing
118 	public Fontset!BitmapType[string]	fontsets;///Fontset name association table
119 	public BitmapType[string]		icons;		///Icon name association table
120 	private StringType				_input;		///The source XML/ETML document
121 	///Constructor with no arguments
122 	public this() @safe pure nothrow {
123 		reset();
124 	}
125 	///Creates a new instance with a select string input.
126 	public this(StringType _input) @safe pure nothrow {
127 		this._input = _input;
128 		__ctor();
129 	}
130 	///Resets the output, but keeps the public parameters.
131 	///Input must be set to default or to new target separately.
132 	public void reset() @safe pure nothrow {
133 		current = new Text!BitmapType("", null);
134 		//_output = current;
135 		stack ~= current;
136 	}
137 	///Sets the default formatting
138 	public CharacterFormattingInfo!BitmapType defaultFormatting(CharacterFormattingInfo!BitmapType val) @property @safe
139 			pure nothrow {
140 		if (stack[0].formatting is null) {
141 			stack[0].formatting = val;
142 		}
143 		defFrmt = val;
144 		return defFrmt;
145 	}
146 	///Gets the default formatting
147 	public CharacterFormattingInfo!BitmapType defaultFormatting()@property @safe pure nothrow @nogc {
148 		return defFrmt;
149 	}
150 	///Returns the root/output element
151 	public Text!BitmapType output() @property @safe @nogc pure nothrow {
152 		return stack[0];
153 	}
154 	///Sets/gets the input
155 	public StringType input() @property @safe @nogc pure nothrow inout {
156 		return _input;
157 	}
158 	/**
159 	 * Parses the formatted text, then returns the output.
160 	 */
161 	public Text!BitmapType parse() @trusted {
162 		import std.conv : to;
163 		static struct XMLHandler(T) {
164 			private void finalizeFormatting() {
165 				if (current.text.length) {	//finalize text chunk if new format is used
166 					//check for duplicates in the previous ones, if there's none, then add the new one
167 					CharacterFormattingInfo!BitmapType f0;
168 					foreach (f; chrFrmt) {
169 						if (f == currFrmt) {
170 							f0 = f;
171 						}
172 					}
173 					if (f0) {
174 						current.formatting = f0;
175 					} else {
176 						current.formatting = currFrmt;
177 						chrFrmt ~= currFrmt;
178 					}
179 
180 					Text!BitmapType nextChunk = new Text!BitmapType(null, current.formatting, null);
181 					current.next = nextChunk;
182 					current = nextChunk;
183 				}
184 			}
185 			//Create a new text instance for every new tag
186 			void onElementStart (ref T node) {
187 				import std.ascii : toLower;
188 				dom.Element!StringType node0 = cast(dom.Element!StringType)node;
189 				void parseLineStrikeFormatting() {
190 					bool updateFormat;
191 					if (node0.hasAttribute("style")) {
192 						switch (toLower(node0.getAttribute("style"))) {
193 							case "normal":
194 								currFrmt.formatFlags = currFrmt.formatFlags & (uint.max ^ FormattingFlags.ulLineStyle);
195 								//currFrmt.formatFlags = currFrmt.formatFlags | FormattingFlags.
196 								break;
197 							case "dotted":
198 								currFrmt.formatFlags = currFrmt.formatFlags & (uint.max ^ FormattingFlags.ulLineStyle);
199 								currFrmt.formatFlags = currFrmt.formatFlags | FormattingFlags.underlineDotted;
200 								break;
201 							case "wavy":
202 								currFrmt.formatFlags = currFrmt.formatFlags & (uint.max ^ FormattingFlags.ulLineStyle);
203 								currFrmt.formatFlags = currFrmt.formatFlags | FormattingFlags.underlineWavy;
204 								break;
205 							case "wavySoft":
206 								currFrmt.formatFlags = currFrmt.formatFlags & (uint.max ^ FormattingFlags.ulLineStyle);
207 								currFrmt.formatFlags = currFrmt.formatFlags | FormattingFlags.underlineWavySoft;
208 								break;
209 							case "stripes":
210 								currFrmt.formatFlags = currFrmt.formatFlags & (uint.max ^ FormattingFlags.ulLineStyle);
211 								currFrmt.formatFlags = currFrmt.formatFlags | FormattingFlags.underlineStripes;
212 								break;
213 							default:
214 								break;
215 						}
216 						updateFormat = true;
217 					}
218 					if (node0.hasAttribute("lines")) {
219 						switch (toLower(node0.getAttribute("lines"))) {
220 							currFrmt.formatFlags = currFrmt.formatFlags & (uint.max ^ FormattingFlags.ulLineMultiplier);
221 							/+case "single":
222 								currFrmt.formatFlags = currFrmt.formatFlags & (uint.max ^ FormattingFlags.ulLineMultiplier);
223 								break;+/
224 							case "double":
225 								currFrmt.formatFlags = currFrmt.formatFlags | FormattingFlags.underlineDouble;
226 								break;
227 							case "triple":
228 								currFrmt.formatFlags = currFrmt.formatFlags | FormattingFlags.underlineTriple;
229 								break;
230 							case "quad", "quadruple":
231 								currFrmt.formatFlags = currFrmt.formatFlags | FormattingFlags.underlineQuadruple;
232 								break;
233 							default:
234 								break;
235 						}
236 						updateFormat = true;
237 					}
238 					if (node0.hasAttribute("perWord")) {
239 						if (toLower(node0.getAttribute("perWord")) == "true") {
240 							currFrmt.formatFlags = currFrmt.formatFlags | FormattingFlags.ulPerWord;
241 						} else {
242 							currFrmt.formatFlags = currFrmt.formatFlags & (uint.max ^ FormattingFlags.ulPerWord);
243 						}
244 						updateFormat = true;
245 					}
246 					return updateFormat;
247 				}
248 				void parseFontFormatting() {
249 					bool updateFormat;
250 					if (node0.hasAttribute("type")) {
251 						currFrmt.fontType = fontsets[node0.getAttribute("type")];
252 						updateFormat = true;
253 					}
254 					if (node0.hasAttribute("color")) {
255 						static if(BitmapType.stringof == Bitmap32Bit.stringof) {
256 							//currFrmt.color =
257 						} else static if(BitmapType.stringof == Bitmap8Bit.stringof) {
258 							currFrmt.color = to!ubyte(node0.getAttribute("color"));
259 						} else static if(BitmapType.stringof == Bitmap16Bit.stringof) {
260 							currFrmt.color = to!ushort(node0.getAttribute("color"));
261 						}
262 						updateFormat = true;
263 					}
264 					return updateFormat;
265 				}
266 				switch (node0.localName) {
267 					case "p":
268 						if (node0.hasAttributes) {
269 							parseFontFormatting();
270 						}
271 						break;
272 					case "u":
273 						currFrmt.formatFlags |= FormattingFlags.underline;
274 						parseLineStrikeFormatting();
275 						break;
276 					case "o":
277 						currFrmt.formatFlags |= FormattingFlags.overline;
278 						parseLineStrikeFormatting();
279 						break;
280 					case "s":
281 						currFrmt.formatFlags |= FormattingFlags.strikeThrough;
282 						parseLineStrikeFormatting();
283 						break;
284 					case "font":
285 						parseFontFormatting();
286 						break;
287 					default:
288 						break;
289 				}
290 				//Start new chunk on beginning then push it to the stack
291 				Text!BitmapType nextChunk = new Text!BitmapType(null, current.formatting, null);
292 				current.next = nextChunk;
293 				current = nextChunk;
294 				stack ~= current;
295 			}
296 			//Finalize the current text instance
297 			void onElementEnd (ref T node) {
298 				//if (node.localName == "p") {//Insert new paragraph character at the end of a paragraph
299 					current ~= FormattingCharacters.newParagraph;
300 				//} else if (node.localName == "font") {
301 
302 				//}
303 				finalizeFormatting();
304 				if(stack.length > 1)
305 					stack.length = stack.length - 1;
306 			}
307 			void onElementEmpty (ref T node) {
308 
309 				dom.Element!StringType node0 = cast(dom.Element!StringType)node;
310 				switch (node0.localName) {
311 					case "br":
312 						current ~= FormattingCharacters.newLine;
313 						break;
314 					case "icon":
315 						if (node0.hasAttributes) {
316 
317 							if (node0.hasAttribute("src")) {
318 								string a = to!string(node0.getAttribute("src"));
319 								if (current.text == "") {
320 									current.icon = icons[a];
321 								} else {
322 									Text!BitmapType nextChunk = new Text!BitmapType(null, current.formatting, null, 0, icons[a]);
323 									current.next = nextChunk;
324 									current = nextChunk;
325 									//stack[$-1] = current;
326 								}
327 							} else {
328 								throw new XMLTextParsingException("Missing src attribute from tag \"</ icon>\"!");
329 							}
330 							if (node0.hasAttribute("offsetX")) {
331 								current.iconOffsetX = to!byte(node0.getAttribute("offsetX"));
332 							}
333 							if (node0.hasAttribute("offsetY")) {
334 								current.iconOffsetY = to!byte(node0.getAttribute("offsetY"));
335 							}
336 						} else {
337 							throw new XMLTextParsingException("Missing src attribute from tag \"</ icon>\"!");
338 						}
339 						break;
340 					default:
341 						break;
342 				}
343 			}
344         	void onProcessingInstruction (ref T node) {	}
345 			//
346 			void onText (ref T node) @safe {
347 				import std.utf : toUTF32;
348 				dom.Text!StringType text = cast(dom.Text!StringType)node;
349 				current.text = text.data;
350 			}
351 			void onDocument (ref T node) {	}
352 			void onComment (ref T node) {	}
353 		}
354 		/+auto xmlParser = xml.lexer.parser.cursor.saxParser!XMLHandler;
355 		xmlParser.setSource(_input);
356 		xmlParser.processDocument();+/
357 		return stack[0];
358 	}
359 }
360 
361 static TextParser!(Bitmap8Bit) test;
362 
363 unittest {
364 	TextParser!(Bitmap8Bit) parser;
365 	string exampleFormattedText = `
366 		<?xml encoding = "utf-8">
367 		<text>
368 			<p>
369 				This is an example formatted text for the PixelPerfectEngine's "concrete" module.
370 				</ br>
371 				This example contains <i>italic</ i>, <u>underline</ u>, <o>overline</ o>, and <s>strike out</ s> text.
372 				<icon src = "Jeffrey"/>
373 				<font color = "15">This text has the color 15.</ font>
374 			</ p>
375 		</ text>
376 	`
377 	parser = new TextParser!(Bitmap8Bit)(exampleFormattedText);
378 }
379 public class XMLTextParsingException : Exception {
380 	@nogc @safe pure nothrow this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable nextInChain = null) {
381 		super(msg, file, line, nextInChain);
382 	}
383 
384 	@nogc @safe pure nothrow this(string msg, Throwable nextInChain, string file = __FILE__, size_t line = __LINE__) {
385 		super(msg, file, line, nextInChain);
386 	}
387 }