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.concrete.text; 8 9 public import PixelPerfectEngine.graphics.fontsets; 10 public import PixelPerfectEngine.graphics.bitmap; 11 12 //import std.xml; 13 import std.experimental.xml.sax; 14 import dom = std.experimental.xml.dom; 15 import std.experimental.xml.lexers; 16 import std.experimental.xml.parser; 17 18 /** 19 * Implements a formatted text chunk, that can be serialized in XML form. 20 * Has a linked list structure to easily link multiple chunks after each other. 21 */ 22 public class Text(BitmapType = Bitmap8Bit) { 23 24 public dstring text; ///The text to be displayed 25 public CharacterFormattingInfo!BitmapType formatting; ///The formatting of this text block 26 public Text next; ///The next piece of formatted text block 27 public int frontTab; ///Space before the text chunk in pixels. Can be negative. 28 public BitmapType icon; ///Icon inserted in front of the text chunk. 29 public byte iconOffsetX; ///X offset of the icon if any 30 public byte iconOffsetY; ///Y offset of 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, Text next = null, int frontTab = 0, 35 BitmapType icon = null) @safe pure @nogc nothrow { 36 this.text = text; 37 this.formatting = formatting; 38 this.next = next; 39 this.frontTab = frontTab; 40 this.icon = icon; 41 } 42 /** 43 * Parses formatted text from binary. 44 * Header is the first 4 bytes of the stream. 45 */ 46 public this(ubyte[] stream, Fontset!BitmapType[uint] fonts) @safe pure { 47 import PixelPerfectEngine.system.etc : reinterpretCast, reinterpretGet; 48 assert (stream.length > 4, "Bytestream too short!"); 49 const uint header = reinterpretGet!uint(stream[0..4]); 50 stream = stream[4..$]; 51 switch (header & 0xF) { 52 case TextType.UTF8: 53 __ctor(TextType.UTF8, stream, fonts); 54 break; 55 case TextType.UTF32: 56 __ctor(TextType.UTF32, stream, fonts); 57 break; 58 default: 59 throw new Exception ("Binary character formatting stream error!"); 60 } 61 } 62 private this(TextType type, ubyte[] stream, Fontset!BitmapType[uint] fonts) @safe pure { 63 import PixelPerfectEngine.system.etc : reinterpretCast, reinterpretGet; 64 import std.utf : toUTF32; 65 //uint header = reinterpretGet!uint(stream[0..4]); 66 //stream = stream[4..$]; 67 if (stream[0] == FormattingCharacters.binaryCFI) { 68 stream = stream[1..$]; 69 Fontset!BitmapType fontType = fonts[reinterpretGet!uint(stream[0..4])]; 70 const ubyte color = stream[7]; 71 const uint formatFlags = reinterpretGet!uint(stream[8..12]); 72 const ushort paragraphSpace = reinterpretGet!ushort(stream[12..14]); 73 const short rowHeight = reinterpretGet!short(stream[14..16]); 74 formatting = new CharacterFormattingInfo!BitmapType(fontType, color, formatFlags, paragraphSpace, rowHeight); 75 stream = stream[16..$]; 76 } else throw new Exception("Binary character formatting stream error!"); 77 if (stream[0] == FormattingCharacters.binaryLI) { 78 stream = stream[1..$]; 79 const uint charStrmL = reinterpretGet!uint(stream[0..4]); 80 if (charStrmL <= stream.length) { 81 stream = stream[4..$]; 82 if(type == TextType.UTF8) 83 text = toUTF32(reinterpretCast!char(stream[0..charStrmL])); 84 else if(type == TextType.UTF32) 85 text = reinterpretCast!dchar(stream[0..charStrmL]); 86 stream = stream[charStrmL..$]; 87 } else throw new Exception("Binary character formatting stream error!"); 88 if (stream.length) { 89 next = new Text!BitmapType(type, stream, fonts); 90 } 91 } 92 } 93 /** 94 * Returns the text as a 32bit string without the formatting. 95 */ 96 public dstring toDString() @safe pure nothrow { 97 if (next) 98 return text ~ next.toDString; 99 else 100 return text; 101 } 102 } 103 104 /** 105 * Parses text from XML/ETML 106 * 107 * See "ETML.md" for info. 108 */ 109 public class TextParser(BitmapType = Bitmap8Bit, StringType = string) 110 /+if((typeof(BitmapType) is Bitmap8Bit || typeof(BitmapType) is Bitmap16Bit || 111 typeof(BitmapType) is Bitmap32Bit) && (typeof(StringType) is string || typeof(StringType) is dstring))+/ { 112 private Text!BitmapType current; ///Current element 113 //private Text!BitmapType _output; ///Root/output element 114 private Text!BitmapType[] stack; ///Mostly to refer back to previous elements 115 private CharacterFormattingInfo!BitmapType currFrmt;///currently parsed formatting 116 public CharacterFormattingInfo!BitmapType[] chrFrmt;///All character format, that has been parsed so far 117 private CharacterFormattingInfo!BitmapType defFrmt;///Default character formatting. Must be set before parsing 118 public Fontset!BitmapType[string] fontsets;///Fontset name association table 119 public BitmapType[string] icons; ///Icon name association table 120 private StringType _input; ///The source XML/ETML document 121 ///Constructor with no arguments 122 public this() @safe pure nothrow { 123 reset(); 124 } 125 ///Creates a new instance with a select string input. 126 public this(StringType _input) @safe pure nothrow { 127 this._input = _input; 128 __ctor(); 129 } 130 ///Resets the output, but keeps the public parameters. 131 ///Input must be set to default or to new target separately. 132 public void reset() @safe pure nothrow { 133 current = new Text!BitmapType("", null); 134 //_output = current; 135 stack ~= current; 136 } 137 ///Sets the default formatting 138 public CharacterFormattingInfo!BitmapType defaultFormatting(CharacterFormattingInfo!BitmapType val) @property @safe 139 pure nothrow { 140 if (stack[0].formatting is null) { 141 stack[0].formatting = val; 142 } 143 defFrmt = val; 144 return defFrmt; 145 } 146 ///Gets the default formatting 147 public CharacterFormattingInfo!BitmapType defaultFormatting()@property @safe pure nothrow @nogc { 148 return defFrmt; 149 } 150 ///Returns the root/output element 151 public Text!BitmapType output() @property @safe @nogc pure nothrow { 152 return stack[0]; 153 } 154 ///Sets/gets the input 155 public StringType input() @property @safe @nogc pure nothrow inout { 156 return _input; 157 } 158 /** 159 * Parses the formatted text, then returns the output. 160 */ 161 public Text!BitmapType parse() @trusted { 162 import std.conv : to; 163 static struct XMLHandler(T) { 164 private void finalizeFormatting() { 165 if (current.text.length) { //finalize text chunk if new format is used 166 //check for duplicates in the previous ones, if there's none, then add the new one 167 CharacterFormattingInfo!BitmapType f0; 168 foreach (f; chrFrmt) { 169 if (f == currFrmt) { 170 f0 = f; 171 } 172 } 173 if (f0) { 174 current.formatting = f0; 175 } else { 176 current.formatting = currFrmt; 177 chrFrmt ~= currFrmt; 178 } 179 180 Text!BitmapType nextChunk = new Text!BitmapType(null, current.formatting, null); 181 current.next = nextChunk; 182 current = nextChunk; 183 } 184 } 185 //Create a new text instance for every new tag 186 void onElementStart (ref T node) { 187 import std.ascii : toLower; 188 dom.Element!StringType node0 = cast(dom.Element!StringType)node; 189 void parseLineStrikeFormatting() { 190 bool updateFormat; 191 if (node0.hasAttribute("style")) { 192 switch (toLower(node0.getAttribute("style"))) { 193 case "normal": 194 currFrmt.formatFlags = currFrmt.formatFlags & (uint.max ^ FormattingFlags.ulLineStyle); 195 //currFrmt.formatFlags = currFrmt.formatFlags | FormattingFlags. 196 break; 197 case "dotted": 198 currFrmt.formatFlags = currFrmt.formatFlags & (uint.max ^ FormattingFlags.ulLineStyle); 199 currFrmt.formatFlags = currFrmt.formatFlags | FormattingFlags.underlineDotted; 200 break; 201 case "wavy": 202 currFrmt.formatFlags = currFrmt.formatFlags & (uint.max ^ FormattingFlags.ulLineStyle); 203 currFrmt.formatFlags = currFrmt.formatFlags | FormattingFlags.underlineWavy; 204 break; 205 case "wavySoft": 206 currFrmt.formatFlags = currFrmt.formatFlags & (uint.max ^ FormattingFlags.ulLineStyle); 207 currFrmt.formatFlags = currFrmt.formatFlags | FormattingFlags.underlineWavySoft; 208 break; 209 case "stripes": 210 currFrmt.formatFlags = currFrmt.formatFlags & (uint.max ^ FormattingFlags.ulLineStyle); 211 currFrmt.formatFlags = currFrmt.formatFlags | FormattingFlags.underlineStripes; 212 break; 213 default: 214 break; 215 } 216 updateFormat = true; 217 } 218 if (node0.hasAttribute("lines")) { 219 switch (toLower(node0.getAttribute("lines"))) { 220 currFrmt.formatFlags = currFrmt.formatFlags & (uint.max ^ FormattingFlags.ulLineMultiplier); 221 /+case "single": 222 currFrmt.formatFlags = currFrmt.formatFlags & (uint.max ^ FormattingFlags.ulLineMultiplier); 223 break;+/ 224 case "double": 225 currFrmt.formatFlags = currFrmt.formatFlags | FormattingFlags.underlineDouble; 226 break; 227 case "triple": 228 currFrmt.formatFlags = currFrmt.formatFlags | FormattingFlags.underlineTriple; 229 break; 230 case "quad", "quadruple": 231 currFrmt.formatFlags = currFrmt.formatFlags | FormattingFlags.underlineQuadruple; 232 break; 233 default: 234 break; 235 } 236 updateFormat = true; 237 } 238 if (node0.hasAttribute("perWord")) { 239 if (toLower(node0.getAttribute("perWord")) == "true") { 240 currFrmt.formatFlags = currFrmt.formatFlags | FormattingFlags.ulPerWord; 241 } else { 242 currFrmt.formatFlags = currFrmt.formatFlags & (uint.max ^ FormattingFlags.ulPerWord); 243 } 244 updateFormat = true; 245 } 246 return updateFormat; 247 } 248 void parseFontFormatting() { 249 bool updateFormat; 250 if (node0.hasAttribute("type")) { 251 currFrmt.fontType = fontsets[node0.getAttribute("type")]; 252 updateFormat = true; 253 } 254 if (node0.hasAttribute("color")) { 255 static if(BitmapType.stringof == Bitmap32Bit.stringof) { 256 //currFrmt.color = 257 } else static if(BitmapType.stringof == Bitmap8Bit.stringof) { 258 currFrmt.color = to!ubyte(node0.getAttribute("color")); 259 } else static if(BitmapType.stringof == Bitmap16Bit.stringof) { 260 currFrmt.color = to!ushort(node0.getAttribute("color")); 261 } 262 updateFormat = true; 263 } 264 return updateFormat; 265 } 266 switch (node0.localName) { 267 case "p": 268 if (node0.hasAttributes) { 269 parseFontFormatting(); 270 } 271 break; 272 case "u": 273 currFrmt.formatFlags |= FormattingFlags.underline; 274 parseLineStrikeFormatting(); 275 break; 276 case "o": 277 currFrmt.formatFlags |= FormattingFlags.overline; 278 parseLineStrikeFormatting(); 279 break; 280 case "s": 281 currFrmt.formatFlags |= FormattingFlags.strikeThrough; 282 parseLineStrikeFormatting(); 283 break; 284 case "font": 285 parseFontFormatting(); 286 break; 287 default: 288 break; 289 } 290 //Start new chunk on beginning then push it to the stack 291 Text!BitmapType nextChunk = new Text!BitmapType(null, current.formatting, null); 292 current.next = nextChunk; 293 current = nextChunk; 294 stack ~= current; 295 } 296 //Finalize the current text instance 297 void onElementEnd (ref T node) { 298 //if (node.localName == "p") {//Insert new paragraph character at the end of a paragraph 299 current ~= FormattingCharacters.newParagraph; 300 //} else if (node.localName == "font") { 301 302 //} 303 finalizeFormatting(); 304 if(stack.length > 1) 305 stack.length = stack.length - 1; 306 } 307 void onElementEmpty (ref T node) { 308 309 dom.Element!StringType node0 = cast(dom.Element!StringType)node; 310 switch (node0.localName) { 311 case "br": 312 current ~= FormattingCharacters.newLine; 313 break; 314 case "icon": 315 if (node0.hasAttributes) { 316 317 if (node0.hasAttribute("src")) { 318 string a = to!string(node0.getAttribute("src")); 319 if (current.text == "") { 320 current.icon = icons[a]; 321 } else { 322 Text!BitmapType nextChunk = new Text!BitmapType(null, current.formatting, null, 0, icons[a]); 323 current.next = nextChunk; 324 current = nextChunk; 325 //stack[$-1] = current; 326 } 327 } else { 328 throw new XMLTextParsingException("Missing src attribute from tag \"</ icon>\"!"); 329 } 330 if (node0.hasAttribute("offsetX")) { 331 current.iconOffsetX = to!byte(node0.getAttribute("offsetX")); 332 } 333 if (node0.hasAttribute("offsetY")) { 334 current.iconOffsetY = to!byte(node0.getAttribute("offsetY")); 335 } 336 } else { 337 throw new XMLTextParsingException("Missing src attribute from tag \"</ icon>\"!"); 338 } 339 break; 340 default: 341 break; 342 } 343 } 344 void onProcessingInstruction (ref T node) { } 345 // 346 void onText (ref T node) @safe { 347 import std.utf : toUTF32; 348 dom.Text!StringType text = cast(dom.Text!StringType)node; 349 current.text = text.data; 350 } 351 void onDocument (ref T node) { } 352 void onComment (ref T node) { } 353 } 354 /+auto xmlParser = xml.lexer.parser.cursor.saxParser!XMLHandler; 355 xmlParser.setSource(_input); 356 xmlParser.processDocument();+/ 357 return stack[0]; 358 } 359 } 360 361 static TextParser!(Bitmap8Bit) test; 362 363 unittest { 364 TextParser!(Bitmap8Bit) parser; 365 string exampleFormattedText = ` 366 <?xml encoding = "utf-8"> 367 <text> 368 <p> 369 This is an example formatted text for the PixelPerfectEngine's "concrete" module. 370 </ br> 371 This example contains <i>italic</ i>, <u>underline</ u>, <o>overline</ o>, and <s>strike out</ s> text. 372 <icon src = "Jeffrey"/> 373 <font color = "15">This text has the color 15.</ font> 374 </ p> 375 </ text> 376 ` 377 parser = new TextParser!(Bitmap8Bit)(exampleFormattedText); 378 } 379 public class XMLTextParsingException : Exception { 380 @nogc @safe pure nothrow this(string msg, string file = __FILE__, size_t line = __LINE__, Throwable nextInChain = null) { 381 super(msg, file, line, nextInChain); 382 } 383 384 @nogc @safe pure nothrow this(string msg, Throwable nextInChain, string file = __FILE__, size_t line = __LINE__) { 385 super(msg, file, line, nextInChain); 386 } 387 }