1 module pixelperfectengine.system.lang.textparser;
2 
3 public import pixelperfectengine.graphics.fontsets;
4 public import pixelperfectengine.graphics.bitmap;
5 
6 public import pixelperfectengine.graphics.text;
7 public import pixelperfectengine.system.exc;
8 public import pixelperfectengine.system.etc : isInteger;
9 
10 import newxml;
11 import newxml.interfaces : XMLException;
12 import std.utf : toUTF32, toUTF8;
13 import std.conv : to;
14 import std.algorithm : countUntil;
15 import std.exception : enforce;
16 import std.typecons : BitFlags;
17 import core.exception : RangeError;
18 
19 /**
20  * Parses text from XML/ETML
21  *
22  * See "ETML.md" for further info on how the ETML format works.
23  */
24 public class TextParserTempl(BitmapType = Bitmap8Bit)
25 		if (is(BitmapType == Bitmap8Bit) || is(BitmapType == Bitmap16Bit) || is(BitmapType Bitmap32Bit)) {
26 	alias TextType = TextTempl!BitmapType;
27 	alias ChrFormat = CharacterFormattingInfo!BitmapType;
28 	public TextType[string]			output;		///All texts found within the ETML file
29 	private TextType				chunkRoot;
30 	private TextType				currTextBlock;
31 	//private ChrFormat				currFrmt;///currently parsed formatting
32 	public ChrFormat[]				chrFrmt;///All character format, that has been parsed so far
33 	public ChrFormat[]				frmtStack;	///Character formatting stack.
34 	public ChrFormat[string]		namedFormats;///Named format lookup table.
35 	public Fontset!BitmapType[string]	fontsets;///Fontset name association table
36 	public BitmapType[string]		icons;		///Icon name association table
37 	public dstring[dstring]			customEntities;///Custom entities that are loaded during parsing.
38 	private dstring					_input;		///The source XML/ETML document
39 	private bool					inTextChunk;///Set to true if currently in a text chunk to allow detection of cascading 
40 	
41 	///Creates a new instance with a select string input.
42 	public this(dstring _input, CharacterFormattingInfo!BitmapType val) @safe pure nothrow {
43 		this._input = _input;
44 		chrFrmt = [val];
45 	}
46 	///Gets the default formatting
47 	public CharacterFormattingInfo!BitmapType defaultFormatting() @property @safe pure nothrow @nogc {
48 		return defFrmt;
49 	}
50 	private final ChrFormat currFrmt() @property @safe pure nothrow @nogc {
51 		return frmtStack[$ - 1];
52 	}
53 	private final ref ChrFormat defFrmt() @property @safe pure nothrow @nogc {
54 		return chrFrmt[0];
55 	}
56 	///Sets/gets the input
57 	public dstring input() @property @safe @nogc pure nothrow inout {
58 		return _input;
59 	}
60 	/**
61 	 * Parses the formatted text, then sets the output values.
62 	 */
63 	public void parse()  {
64 		auto parser = _input.lexer.parser.cursor.saxParser;
65 		parser.setSource(_input);
66 		parser.onElementStart = &onElementStart;
67 		parser.onElementEnd = &onElementEnd;
68 		try {
69 			parser.processDocument();
70 		} catch (XMLException e) {	//XML formatting issue
71 			throw new XMLTextParsingException("XML file is badly formatted!", e);
72 		} catch (RangeError e) {	//Missing mandatory attributes
73 			throw new XMLTextParsingException("Missing mandatory attribute found!", e);
74 		}
75 	}
76 	protected void onText(dstring content) @safe {
77 		if (currTextBlock.charLength) {
78 			currTextBlock.next = new TextType(content, currFrmt);
79 			currTextBlock = currTextBlock.next;
80 		} else {
81 			currTextBlock.text = content;
82 		}
83 	}
84 	protected void onElementStart(dstring name, dstring[dstring] attr) @safe {
85 		switch (name) {
86 			case "text":
87 				onTextElementStart(attr);
88 				break;
89 			case "p":
90 				onPElementStart(attr);
91 				break;
92 			case "u":
93 				onLineFormatElementStart!"u"(attr);
94 				break;
95 			case "s":
96 				onLineFormatElementStart!"s"(attr);
97 				break;
98 			case "o":
99 				onLineFormatElementStart!"o"(attr);
100 				break;
101 			case "i":
102 				onIElementStart(attr);
103 				break;
104 			default:
105 				break;
106 		}
107 	}
108 	protected void onElementEmpty(dstring name, dstring[dstring] attr) @safe {
109 		switch (name) {
110 			case "br":
111 				onBrElement();
112 				break;
113 			case "image":
114 				onImageElement(attr);
115 				break;
116 			case "frontTab", "ft":
117 				onFrontTabElement(attr);
118 				break;
119 			case "formatDef":
120 				onFormatDefElement(attr);
121 				break;
122 			default:
123 				break;
124 		}
125 	}
126 	protected void onElementEnd(dstring name) @safe {
127 		switch (name) {
128 			case "p":
129 			case "u":
130 			case "s":
131 			case "o":
132 			case "i":
133 			case "font":
134 			case "format":
135 				closeFormattingTag();
136 				break;
137 			default:
138 				break;
139 		}
140 	}
141 	/** 
142 	 * Begins a new text chain, and flushes the font stack.
143 	 * Params:
144 	 *   attributes = Attributes are received here from textparser.
145 	 */
146 	protected void onTextElementStart(dstring[dstring] attributes) @safe {
147 		string textID = toUTF8(attributes["id"]);
148 		currTextBlock = new TextType(null, defFrmt);
149 		//currFrmt = defFrmt;
150 		frmtStack = [defFrmt];
151 		//Add first chunk to the output, so it can be recalled later on.
152 		output[textID] = currTextBlock;
153 	}
154 	protected void onPElementStart(dstring[dstring] attributes) @safe {
155 		currTextBlock.next = new TextType(null, currFrmt);
156 		currTextBlock = currTextBlock.next;
157 		ChrFormat newFrmt = new ChrFormat(currFrmt);
158 		dstring paragraphSpaceStr = attributes.get("paragraphSpace", null);
159 		if (paragraphSpaceStr.isInteger) {
160 			newFrmt.paragraphSpace = paragraphSpaceStr.to!ushort;
161 		}
162 		dstring rowHeightStr = attributes.get("rowHeight", null);
163 		if (rowHeightStr.isInteger) {
164 			newFrmt.rowHeight = rowHeightStr.to!short;
165 		}
166 		testFormatting(newFrmt);
167 		currTextBlock.formatting = currFrmt;
168 		currTextBlock.flags.newParagraph = true;
169 	}
170 	protected void onLineFormatElementStart(string Type)(dstring[dstring] attributes) @safe {
171 		closeTextBlockIfNotEmpty();
172 		ChrFormat newFrmt = new ChrFormat(currFrmt);
173 		static if (Type == "u") {
174 			dstring style = attributes.get("style", null);
175 			newFrmt.formatFlags |= FormattingFlags.underline;
176 			if (style.length)
177 				newFrmt.formatFlags &= ~FormattingFlags.ulLineMultiplier;
178 			switch (style) {
179 				case "double":
180 					newFrmt.formatFlags |= FormattingFlags.underlineDouble;
181 					break;
182 				case "triple":
183 					newFrmt.formatFlags |= FormattingFlags.underlineDouble;
184 					break;
185 				case "quad":
186 					newFrmt.formatFlags |= FormattingFlags.underlineDouble;
187 					break;
188 				default:
189 					break;
190 			}
191 			dstring lines = attributes.get("lines", null);
192 			if (lines.length)
193 				newFrmt.formatFlags &= ~FormattingFlags.ulLineStyle;
194 			switch (lines) {
195 				case "dotted":
196 					newFrmt.formatFlags |= FormattingFlags.underlineDouble;
197 					break;
198 				case "wavy":
199 					newFrmt.formatFlags |= FormattingFlags.underlineWavy;
200 					break;
201 				case "wavySoft":
202 					newFrmt.formatFlags |= FormattingFlags.underlineWavySoft;
203 					break;
204 				case "stripes":
205 					newFrmt.formatFlags |= FormattingFlags.underlineStripes;
206 					break;
207 				default:
208 					break;
209 			}
210 			dstring perWord = attributes.get("perWord", null);
211 			if (perWord == "true" || perWord == "yes") {
212 				newFrmt.formatFlags |= FormattingFlags.underlinePerWord;
213 			} else {
214 				newFrmt.formatFlags &= ~FormattingFlags.underlinePerWord;
215 			}
216 		} else static if (Type == "s") {
217 			newFrmt.formatFlags |= FormattingFlags.strikeThrough;
218 		} else static if (Type == "o") {
219 			newFrmt.formatFlags |= FormattingFlags.overline;
220 		}
221 		testFormatting(newFrmt);
222 		currTextBlock.formatting = currFrmt;
223 	}
224 	protected void onIElementStart(dstring[dstring] attributes) @safe {
225 		closeTextBlockIfNotEmpty();
226 		ChrFormat newFrmt = new ChrFormat(currFrmt);
227 		dstring amount = attributes.get("amount", null);
228 		newFrmt.formatFlags &= ~FormattingFlags.forceItalicsMask;
229 		switch (amount) {
230 			case "1/3":
231 				newFrmt.formatFlags |= FormattingFlags.forceItalics1per3;
232 				break;
233 			case "1/2":
234 				newFrmt.formatFlags |= FormattingFlags.forceItalics1per2;
235 				break;
236 			default:	//case "1/4":
237 				newFrmt.formatFlags |= FormattingFlags.forceItalics1per4;
238 				break;
239 		}
240 		testFormatting(newFrmt);
241 		currTextBlock.formatting = currFrmt;
242 	}
243 	protected void onFontElementStart(dstring[dstring] attributes) @safe {
244 		closeTextBlockIfNotEmpty();
245 		ChrFormat newFrmt = new ChrFormat(currFrmt);
246 		string type = toUTF8(attributes.get("font", null));
247 		if (type.length) {
248 			newFrmt.font = fontsets[type];
249 		}
250 		string color = toUTF8(attributes.get("color", null));
251 		if (color.length) {
252 			static if (is(BitmapType == Bitmap8Bit)) {
253 				newFrmt.color = color.to!ubyte();
254 			} else static if (is(BitmapType == Bitmap16Bit)) {
255 				newFrmt.color = color.to!ushort();
256 			} else {
257 
258 			}
259 		}
260 		testFormatting(newFrmt);
261 		currTextBlock.formatting = currFrmt;
262 	}
263 	protected void onFormatElementStart(dstring[dstring] attributes) @safe {
264 		closeTextBlockIfNotEmpty();
265 		currTextBlock.formatting = namedFormats[toUTF8(attributes["id"])];
266 	}
267 	protected void onFormatDefElement(dstring[dstring] attributes) @safe {
268 		bool checkBoolean(dstring val) @safe {
269 			if (val == "true" || val == "yes")
270 				return true;
271 			else
272 				return false;
273 		}
274 		closeTextBlockIfNotEmpty();
275 		ChrFormat newFrmt = new ChrFormat(currFrmt);
276 		foreach (dstring key, dstring elem; attributes) {
277 			switch (key) {
278 				case "u":
279 					if (checkBoolean(elem))
280 						newFrmt.formatFlags &= ~FormattingFlags.underline;
281 					else
282 						newFrmt.formatFlags |= FormattingFlags.underline;
283 					break;
284 				case "s":
285 					if (checkBoolean(elem))
286 						newFrmt.formatFlags &= ~FormattingFlags.strikeThrough;
287 					else
288 						newFrmt.formatFlags |= FormattingFlags.strikeThrough;
289 					break;
290 				case "o":
291 					if (checkBoolean(elem))
292 						newFrmt.formatFlags &= ~FormattingFlags.overline;
293 					else
294 						newFrmt.formatFlags |= FormattingFlags.overline;
295 					break;
296 				case "i":
297 					if (checkBoolean(elem)) {
298 						newFrmt.formatFlags &= ~FormattingFlags.forceItalicsMask;
299 					} else {
300 						dstring i_amount = attributes.get("i_amount", null);
301 						switch (i_amount) {
302 							case "1/2":
303 								newFrmt.formatFlags |= FormattingFlags.forceItalics1per2;
304 								break;
305 							case "1/3":
306 								newFrmt.formatFlags |= FormattingFlags.forceItalics1per3;
307 								break;
308 							default:
309 								newFrmt.formatFlags |= FormattingFlags.forceItalics1per4;
310 								break;
311 						}
312 					}
313 					break;
314 				default:
315 					break;
316 			}
317 		}
318 		const sizediff_t frmtNum = hasChrFormatting(newFrmt);
319 		if (frmtNum == -1) {
320 			namedFormats[toUTF8(attributes["id"])] = newFrmt;
321 			chrFrmt ~= newFrmt;
322 		} else {
323 			namedFormats[toUTF8(attributes["id"])] = chrFrmt[frmtNum];
324 		}
325 	}
326 	protected void onBrElement() @safe {
327 		currTextBlock.next = new TextType(null, currFrmt);
328 		currTextBlock = currTextBlock.next;
329 		currTextBlock.flags.newLine = true;
330 	}
331 	protected void onFrontTabElement(dstring[dstring] attributes) @safe {
332 		closeTextBlockIfNotEmpty();
333 		currTextBlock.frontTab = attributes["amount"].to!int;
334 	}
335 	protected void onImageElement(dstring[dstring] attributes) @safe {
336 		if (currTextBlock.charLength || currTextBlock.icon) {
337 			currTextBlock.next = new TextType(null, currFrmt);
338 			currTextBlock = currTextBlock.next;
339 		}
340 	}
341 	protected void closeFormattingTag() @safe {
342 		removeTopFromFrmtStack();
343 		closeTextBlockIfNotEmpty();
344 	}
345 	protected final void closeTextBlockIfNotEmpty() @safe {
346 		if (currTextBlock.charLength) {
347 			currTextBlock.next = new TextType(null, currFrmt);
348 			currTextBlock = currTextBlock.next;
349 		}
350 	}
351 	protected final void removeTopFromFrmtStack() @safe {
352 		frmtStack = frmtStack[0..$-1];
353 		enforce!XMLTextParsingException(frmtStack.length >= 1, "Formatting stack is empty!");
354 	}
355 	protected final void testFormatting(ChrFormat frmt) @safe {
356 		sizediff_t frmtNum = hasChrFormatting(frmt);
357 		if (frmtNum == -1) {	///New formatting found, add this to overall formatting
358 			chrFrmt ~= frmt;
359 			frmtStack ~= frmt;
360 		} else {				///Already used formatting found, use that instead
361 			frmtStack ~= chrFrmt[frmtNum];
362 		}
363 	}
364 	protected final sizediff_t hasChrFormatting(ChrFormat frmt) @trusted {
365 		return countUntil(chrFrmt, frmt);
366 	}
367 }
368 alias TextParser = TextParserTempl!Bitmap8Bit;
369 public class XMLTextParsingException : PPEException {
370 	@nogc @safe pure nothrow this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable nextInChain = null)
371 			{
372 		super(msg, file, line, nextInChain);
373 	}
374 
375 	@nogc @safe pure nothrow this(string msg, Throwable nextInChain, string file = __FILE__, size_t line = __LINE__) {
376 		super(msg, file, line, nextInChain);
377 	}
378 }