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