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.graphics.text;
8 
9 public import PixelPerfectEngine.graphics.fontsets;
10 public import PixelPerfectEngine.graphics.bitmap;
11 
12 import xml = undead.xml;
13 import std.utf : toUTF32, toUTF8;
14 import std.conv : to;
15 import std.algorithm : countUntil;
16 
17 /**
18  * Implements a formatted text chunk, that can be serialized in XML form.
19  * Has a linked list structure to easily link multiple chunks after each other.
20  */
21 public class TextTempl(BitmapType = Bitmap8Bit) {
22 
23 	protected dchar[]		_text;			///The text to be displayed
24 	public CharacterFormattingInfo!BitmapType 	formatting;	///The formatting of this text block
25 	public TextTempl!BitmapType	next;			///The next piece of formatted text block
26 	public int				frontTab;		///Space before the text chunk in pixels. Can be negative.
27 	public BitmapType		icon;			///Icon inserted in front of the text chunk.
28 	public byte				iconOffsetX;	///X offset of the icon if any
29 	public byte				iconOffsetY;	///Y offset of the icon if any
30 	public byte				iconSpacing;	///Spacing after 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, TextTempl!BitmapType next = null, int frontTab = 0,
35 			BitmapType icon = null) @safe pure nothrow {
36 		this.text = text;
37 		this.formatting = formatting;
38 		this.next = next;
39 		this.frontTab = frontTab;
40 		this.icon = icon;
41 	}
42 	///Copy CTOR
43 	public this(TextTempl!BitmapType orig) @safe pure nothrow {
44 		this.text = orig.text.dup;
45 		this.formatting = orig.formatting;
46 		if(orig.next)
47 			this.next = new TextTempl!BitmapType(orig.next);
48 		this.frontTab = orig.frontTab;
49 		this.icon = orig.icon;
50 		this.iconOffsetX = orig.iconOffsetX;
51 		this.iconOffsetY = orig.iconOffsetY;
52 		this.iconSpacing = orig.iconSpacing;
53 	}
54 	/**
55 	 * Returns the text as a 32bit string without the formatting.
56 	 */
57 	public dstring toDString() @safe pure nothrow {
58 		if (next)
59 			return text ~ next.toDString();
60 		else
61 			return text;
62 	}
63 	/**
64 	 * Indexing to refer to child items.
65 	 * Returns null if the given element isn't available.
66 	 */
67 	public Text!BitmapType opIndex(size_t index) @safe pure nothrow {
68 		if (index) {
69 			if (next) {
70 				return next[index - 1];
71 			} else {
72 				return null;
73 			}
74 		} else {
75 			return this;
76 		}
77 	}
78 	/**
79 	 * Returns the character lenght.
80 	 */
81 	public @property size_t charLength() @safe pure nothrow @nogc const {
82 		if (next) return _text.length + next.charLength;
83 		else return _text.length;
84 	}
85 	/**
86 	 * Removes the character at the given position.
87 	 * Returns the removed character if within bound, or dchar.init if not.
88 	 */
89 	public dchar removeChar(size_t pos) @safe pure {
90 		import std.algorithm.mutation : remove;
91 		void _removeChar() @safe pure {
92 			if(pos == 0) {
93 				_text = _text[1..$];
94 			} else if(pos == _text.length - 1) {
95 				if(_text.length) _text.length = _text.length - 1;
96 			} else {
97 				_text = _text[0..pos] ~ _text[(pos + 1)..$];
98 			}
99 		}
100 		if(pos < _text.length) {
101 			const dchar result = _text[pos];
102 			_removeChar();/+text = text.remove(pos);+/
103 			return result;
104 		} else if(next) {
105 			return next.removeChar(pos - _text.length);
106 		} else return dchar.init;
107 	}
108 	/**
109 	 * Removes a range of characters described by the begin and end position.
110 	 */
111 	public void removeChar(size_t begin, size_t end) @safe pure {
112 		for (size_t i = begin ; i < end ; i++) {
113 			removeChar(begin);
114 		}
115 	}
116 	/**
117 	 * Inserts a given character at the given position.
118 	 * Return the inserted character if within bound, or dchar.init if position points to a place where it
119 	 * cannot be inserted easily.
120 	 */
121 	public dchar insertChar(size_t pos, dchar c) @trusted pure {
122 		import std.array : insertInPlace;
123 		if(pos <= _text.length) {
124 			_text.insertInPlace(pos, c);
125 			return c;
126 		} else if(next) {
127 			return next.insertChar(pos - _text.length, c);
128 		} else return dchar.init;
129 	}
130 	/**
131 	 * Overwrites a character at the given position.
132 	 * Returns the original character if within bound, or or dchar.init if position points to a place where it
133 	 * cannot be inserted easily.
134 	 */
135 	public dchar overwriteChar(size_t pos, dchar c) @safe pure {
136 		if(pos < _text.length) {
137 			const dchar orig = _text[pos];
138 			_text[pos] = c;
139 			return orig;
140 		} else if(next) {
141 			return next.overwriteChar(pos - _text.length, c);
142 		} else if (pos == _text.length) {
143 			_text ~= c;
144 			return c;
145 		} else return dchar.init;
146 	}
147 	/**
148 	 * Returns a character from the given position.
149 	 */
150 	public dchar getChar(size_t pos) @safe pure {
151 		if(pos < _text.length) {
152 			return _text[pos];
153 		} else if(next) {
154 			return next.getChar(pos - _text.length);
155 		} else return dchar.init;
156 	}
157 	/**
158 	 * Returns the width of the current text block in pixels.
159 	 */
160 	public int getBlockWidth() @safe pure nothrow {
161 		auto f = font;
162 		dchar prev;
163 		int localWidth = frontTab;
164 		foreach (c; _text) {
165 			localWidth += f.chars(c).xadvance + formatting.getKerning(prev, c);
166 			prev = c;
167 		}
168 		if(icon) localWidth += iconOffsetX + iconSpacing;
169 		return localWidth;
170 	}
171 	/**
172 	 * Returns the width of the text chain in pixels.
173 	 */
174 	public int getWidth() @safe pure nothrow {
175 		auto f = font;
176 		dchar prev;
177 		int localWidth = frontTab;
178 		foreach (c; _text) {
179 			localWidth += f.chars(c).xadvance + formatting.getKerning(prev, c);
180 			prev = c;
181 		}
182 		if(icon) localWidth += iconOffsetX + iconSpacing;
183 		if(next) return localWidth + next.getWidth();
184 		else return localWidth;
185 	}
186 	/**
187 	 * Returns the width of a slice of the text chain in pixels.
188 	 */
189 	public int getWidth(sizediff_t begin, sizediff_t end) @safe pure {
190 		if(end > _text.length && next is null) 
191 			throw new Exception("Text boundary have been broken!");
192 		int localWidth;
193 		if (!begin) {
194 			localWidth += frontTab;
195 			if (icon)
196 				localWidth += iconOffsetX + iconSpacing;
197 		}
198 		if (begin < _text.length) {
199 			auto f = font;
200 			dchar prev;
201 			foreach (c; _text[begin..end]) {
202 				localWidth += f.chars(c).xadvance + formatting.getKerning(prev, c);
203 				prev = c;
204 			}
205 		}
206 		begin -= _text.length;
207 		end -= _text.length;
208 		if (begin < 0) begin = 0;
209 		if (next && end > 0) return localWidth + next.getWidth(begin, end);
210 		else return localWidth;
211 	}
212 	/**
213 	 * Returns the number of characters fully offset by the amount of pixel.
214 	 */
215 	public int offsetAmount(int pixel) @safe pure nothrow {
216 		int chars;
217 		dchar prev;
218 		while (chars < _text.length && pixel - font.chars(_text[chars]).xadvance + formatting.getKerning(prev, _text[chars]) > 0) {
219 			pixel -= font.chars(_text[chars]).xadvance + formatting.getKerning(prev, _text[chars]);
220 			prev = _text[chars];
221 			chars++;
222 		}
223 		if (chars == _text.length && pixel > 0 && next) 
224 			chars += next.offsetAmount(pixel);
225 		return chars;
226 	}
227 	/**
228 	 * Returns the used font type.
229 	 */
230 	public Fontset!BitmapType font() @safe @nogc pure nothrow {
231 		return formatting.font;
232 	}
233 	///Text accessor
234 	public @property dstring text() @safe pure nothrow {
235 		return _text.idup;
236 	}
237 	///Text accessor
238 	public @property dstring text(dstring val) @safe pure nothrow {
239 		_text = val.dup;
240 		return val;
241 	}
242 }
243 alias Text = TextTempl!Bitmap8Bit;
244 /**
245  * Parses text from XML/ETML
246  *
247  * See "ETML.md" for info.
248  *
249  * Constraints:
250  * * Due to the poor documentation of the replacement XML libraries, I have to use Phobos's own and outdated library.
251  * * <text> chunks are mandatory with ID.
252  * * Currently line formatting (understrike, etc.) is not supported, and every line uses default formatting.
253  */
254 public class TextParserTempl(BitmapType = Bitmap8Bit)
255 		/+if((typeof(BitmapType) is Bitmap8Bit || typeof(BitmapType) is Bitmap16Bit ||
256 		typeof(BitmapType) is Bitmap32Bit) && (typeof(StringType) is string || typeof(StringType) is dstring))+/ {
257 	//private Text!BitmapType	 		current;	///Current element
258 	//private Text!BitmapType			_output;	///Root/output element
259 	//private Text!BitmapType[] 		stack;		///Mostly to refer back to previous elements
260 	public TextTempl!BitmapType[string]	output;		///All texts found within the ETML file
261 	private TextTempl!BitmapType chunkRoot;
262 	private TextTempl!BitmapType currTextBlock;
263 	private CharacterFormattingInfo!BitmapType	currFrmt;///currently parsed formatting
264 	public CharacterFormattingInfo!BitmapType[]	chrFrmt;///All character format, that has been parsed so far
265 	public Fontset!BitmapType[]		fontStack;	///Fonttype formatting stack. Used for referring back to previous font types.
266 	public uint[]					colorStack;
267 	private CharacterFormattingInfo!BitmapType	defFrmt;///Default character formatting. Must be set before parsing
268 	public Fontset!BitmapType[string]	fontsets;///Fontset name association table
269 	public BitmapType[string]		icons;		///Icon name association table
270 	private string					_input;		///The source XML/ETML document
271 	///Constructor with no arguments
272 	public this() @safe pure nothrow {
273 		reset();
274 	}
275 	///Creates a new instance with a select string input.
276 	public this(string _input) @safe pure nothrow {
277 		this._input = _input;
278 		__ctor();
279 	}
280 	///Resets the output, but keeps the public parameters.
281 	///Input must be set to default or to new target separately.
282 	public void reset() @safe pure nothrow {
283 		current = new Text!BitmapType("", null);
284 		//_output = current;
285 		stack ~= current;
286 	}
287 	///Sets the default formatting
288 	public CharacterFormattingInfo!BitmapType defaultFormatting(CharacterFormattingInfo!BitmapType val) @property @safe
289 			pure nothrow {
290 		if (stack[0].formatting is null) {
291 			stack[0].formatting = val;
292 		}
293 		defFrmt = val;
294 		return defFrmt;
295 	}
296 	///Gets the default formatting
297 	public CharacterFormattingInfo!BitmapType defaultFormatting()@property @safe pure nothrow @nogc {
298 		return defFrmt;
299 	}
300 	///Returns the root/output element
301 	/+public Text!BitmapType output() @property @safe @nogc pure nothrow {
302 		return stack[0];
303 	}+/
304 	///Sets/gets the input
305 	public string input() @property @safe @nogc pure nothrow inout {
306 		return _input;
307 	}
308 	/**
309 	 * Parses the formatted text, then returns the output.
310 	 */
311 	public void parse() @trusted {
312 		xml.check(_input);
313 		string currTextChunkID;
314 		currFrmt = new CharacterFormattingInfo!BitmapType(defFrmt);
315 		fontStack ~= currFrmt.fontType;
316 		colorStack ~= currFrmt.color;
317 		auto parser = new xml.DocumentParser(_input);
318 		parser.onStartTag["text"] = (xml.ElementParser parser) {
319 			currTextChunkID = parser.tag.attr["id"];
320 			chunkRoot = new TextTempl!BitmapType();
321 			currTextBlock = chunkRoot;
322 			
323 			output[currTextChunkID] = chunkRoot;
324 			parseRecursively(parser);
325 		};
326 		parser.parse;
327 	}
328 	//block for parsing
329 	private void onText(string s) {
330 		currTextBlock.text ~= toUTF32(s);
331 	}
332 	private void onEndTag_br(xml.Element e) {
333 		currTextBlock.text ~= FormattingCharacters.newLine;
334 	}
335 	private void onStartTag_p(xml.ElementParser parser) {
336 		currTextBlock.text ~= FormattingCharacters.newParagraph;
337 		if(parser.tag.attr.length) {
338 			currFrmt.paragraphSpace = parser.tag.attr.get("paragraphSpace", defFrmt.paragraphSpace);
339 			currFrmt.rowHeight = parser.tag.attr.get("rowHeight", defFrmt.paragraphSpace);
340 			createNextTextChunk();
341 		}
342 		parseRecursively (parser);
343 	}
344 	private void onStartTag_u(xml.ElementParser parser) {
345 		currFrmt.formatFlags |= FormattingFlags.underline;
346 		createNextTextChunk();
347 		parseRecursively (parser);
348 	}
349 	private void onStartTag_s(xml.ElementParser parser) {
350 		currFrmt.formatFlags |= FormattingFlags.strikeThrough;
351 		createNextTextChunk();
352 		parseRecursively (parser);
353 	}
354 	private void onStartTag_o(xml.ElementParser parser) {
355 		currFrmt.formatFlags |= FormattingFlags.overline;
356 		createNextTextChunk();
357 		parseRecursively (parser);
358 	}
359 	private void onStartTag_i(xml.ElementParser parser) {
360 		const string amount = parser.tag.attr.get("amount", "0");
361 		currFrmt.formatFlags &= !FormattingFlags.forceItalicsMask;
362 		switch(amount) {
363 			case "1/2":
364 				currFrmt.formatFlags |= FormattingFlags.forceItalics1per2;
365 				break;
366 			case "1/3":
367 				currFrmt.formatFlags |= FormattingFlags.forceItalics1per3;
368 				break;
369 			case "1/4":
370 				currFrmt.formatFlags |= FormattingFlags.forceItalics1per4;
371 				break;
372 			default:
373 				currFrmt.formatFlags |= defFrmt.formatFlags & FormattingFlags.forceItalicsMask;
374 				break;
375 		}
376 		createNextTextChunk();
377 		parseRecursively (parser);
378 	}
379 	private void onStartTag_font(xml.ElementParser parser) {
380 		const string fontName = parser.tag.attr.get("type", "");
381 		const uint color = to!uint(parser.tag.attr.get(color, colorStack[$-1]));
382 		Fontset!BitmapType fontType;
383 		if(fontType.length) {
384 			fontType = fontsets.get(fontName, null);
385 			if (fontType is null)
386 				throw new XMLTextParsingException("Unknown fonttype!");
387 		} else {
388 			fontType = fontStack[$-1];
389 		}
390 		fontStack ~= fontType;
391 		colorStack ~= color;
392 		currFrmt.color = cast(ubyte)color;
393 		currFrmt.fontType = fontType;
394 		createNextTextChunk();
395 		parseRecursively (parser);
396 	}
397 	private void onEndTag_u(xml.Element e) {
398 		currFrmt.formatFlags &= !FormattingFlags.underline;
399 		createNextTextChunk();
400 	}
401 	private void onEndTag_s(xml.Element e) {
402 		currFrmt.formatFlags &= !FormattingFlags.strikeThrough;
403 		createNextTextChunk();
404 	}
405 	private void onEndTag_o(xml.Element e) {
406 		currFrmt.formatFlags &= !FormattingFlags.overline;
407 		createNextTextChunk();
408 	}
409 	private void onEndTag_i(xml.Element e) {
410 		currFrmt.formatFlags &= !FormattingFlags.forceItalicsMask;
411 		createNextTextChunk();
412 	}
413 	private void onEndTag_font(xml.Element e) {
414 		currFrmt.color = cast(ubyte)colorStack[$-2];
415 		currFrmt.fontType = fontStack[$-2];
416 		colorStack.length--;
417 		fontStack.length--;
418 		createNextTextChunk();
419 	}
420 	private void onStartTag_frontTab(xml.ElementParser parser) {
421 		parseRecursively (parser);
422 	}
423 	private void onStartTag_image(xml.ElementParser parser) {
424 		parseRecursively (parser);
425 	}
426 	///Creates the next text chunk if needed, and also checks the formatting stack.
427 	///Called by the appropriate functions.
428 	///Also handles the character formatting stack
429 	private void createNextTextChunk() {
430 		if(currTextBlock.text.length || currTextBlock.icon !is null){
431 			currTextBlock = new TextTempl!BitmapType(null, null, currTextBlock);
432 			const ptrdiff_t frmtPos = countUntil(chrFrmt, currFrmt);
433 			if (frmtPos == -1){ 
434 				chrFrmt ~= new CharacterFormattingInfo(currFrmt);
435 				currTextBlock.formatting = chrFrmt[$-1];
436 			} else {
437 				currTextBlock.formatting = chrFrmt[frmtPos];
438 			}
439 		}
440 	}
441 	///Called for every instance when there might be additional tags to be parsed.
442 	private void parseRecursively(xml.ElementParser parser) {
443 		parser.onText = &onText;
444 		parser.onEndTag["br"] = &onEndTag_br;
445 		parser.onStartTag["p"] = &onStartTag_p;
446 		parser.parse;
447 	}
448 }
449 
450 public class XMLTextParsingException : Exception {
451 	@nogc @safe pure nothrow this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable nextInChain = null) {
452 		super(msg, file, line, nextInChain);
453 	}
454 
455 	@nogc @safe pure nothrow this(string msg, Throwable nextInChain, string file = __FILE__, size_t line = __LINE__) {
456 		super(msg, file, line, nextInChain);
457 	}
458 }