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 }