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 }