Merge pull request #2417 from luser/fix-weak-map-keys
[shumway.git] / src / TextContent.ts
blobaaa2be7286a39bde85ac4ce14b6a4bbd0833f472
1 /**
2  * Copyright 2014 Mozilla Foundation
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
17 module Shumway {
18   import notImplemented = Shumway.Debug.notImplemented;
19   import somewhatImplemented = Shumway.Debug.somewhatImplemented;
21   import Bounds = Shumway.Bounds;
22   import DataBuffer = Shumway.ArrayUtilities.DataBuffer;
23   import ColorUtilities = Shumway.ColorUtilities;
24   import flash = Shumway.AVMX.AS.flash;
25   import altTieBreakRound = Shumway.NumberUtilities.altTieBreakRound;
27   export const enum TextContentFlags {
28     None            = 0x0000,
29     DirtyBounds     = 0x0001,
30     DirtyContent    = 0x0002,
31     DirtyStyle      = 0x0004,
32     DirtyFlow       = 0x0008,
33     Dirty           = DirtyBounds | DirtyContent | DirtyStyle | DirtyFlow
34   }
36   var _decodeHTMLMap = {
37     lt:   '<',
38     gt:   '>',
39     amp:  '&',
40     quot: '"',
41     apos: "'",
42     nbsp: "\u00A0"
43   };
45   /**
46    * Decodes strings of the format:
47    *
48    * &#0000;
49    * &#x0000;
50    * &#x0000;
51    * &amp;
52    * &lthello
53    *
54    * This is complete enough to handle encoded HTML produced by the Flash IDE.
55    */
56   function decodeHTML(s: string): string {
57     var r = "";
58     for (var i = 0; i < s.length; i++) {
59       var c = s.charAt(i);
60       if (c !== '&') {
61         r += c;
62       } else {
63         // Look for the first '&' or ';', both of these can terminate
64         // the current char code.
65         var j = StringUtilities.indexOfAny(s, ['&', ';'], i + 1);
66         if (j > 0) {
67           var v = s.substring(i + 1, j);
68           if (v.length > 1 && v.charAt(0) === "#") {
69             var n = 0;
70             if (v.length > 2 && v.charAt(1) === "x") {
71               n = parseInt(v.substring(1));
72             } else {
73               n = parseInt(v.substring(2), 16);
74             }
75             r += String.fromCharCode(n);
76           } else {
77             if (_decodeHTMLMap[v] !== undefined) {
78               r += _decodeHTMLMap[v];
79             } else {
80               Debug.unexpected(v);
81             }
82           }
83           i = j;
84         } else {
85           // Flash sometimes generates entities that don't have terminators,
86           // like &bthello. Strong bad sometimes encodes this that way.
87           for (var k in _decodeHTMLMap) {
88             if (s.indexOf(k, i + 1) === i + 1) {
89               r += _decodeHTMLMap[k];
90               i += k.length;
91               break;
92             }
93           }
94         }
95       }
96     }
97     return r;
98   }
100   export class TextContent implements Shumway.Remoting.IRemotable {
101     _id: number;
102     sec: ISecurityDomain;
104     private _bounds: Bounds;
105     private _plainText: string;
106     private _backgroundColor: number;
107     private _borderColor: number;
108     private _autoSize: number;
109     private _wordWrap: boolean;
110     private _scrollV: number;
111     private _scrollH: number;
113     flags: number;
114     defaultTextFormat: flash.text.TextFormat;
115     textRuns: flash.text.TextRun[];
116     textRunData: DataBuffer;
117     matrix: flash.geom.Matrix;
118     coords: number[];
120     constructor(sec: ISecurityDomain, defaultTextFormat?: flash.text.TextFormat) {
121       this.sec = sec;
122       this._id = sec.flash.display.DisplayObject.axClass.getNextSyncID();
123       this._bounds = new Bounds(0, 0, 0, 0);
124       this._plainText = '';
125       this._backgroundColor = 0;
126       this._borderColor = 0;
127       this._autoSize = 0;
128       this._wordWrap = false;
129       this._scrollV = 1;
130       this._scrollH = 0;
131       this.flags = TextContentFlags.None;
132       this.defaultTextFormat = defaultTextFormat || new sec.flash.text.TextFormat();
133       this.textRuns = [];
134       this.textRunData = new DataBuffer();
135       this.matrix = null;
136       this.coords = null;
137     }
139     parseHtml(htmlText: string, styleSheet: flash.text.StyleSheet, multiline: boolean) {
140       var plainText = '';
141       var textRuns = this.textRuns;
142       textRuns.length = 0;
144       var beginIndex = 0;
145       var endIndex = 0;
146       var textFormat = this.defaultTextFormat.clone();
147       var prevTextRun: flash.text.TextRun = null;
148       var stack = [];
150       var handler: HTMLParserHandler;
151       Shumway.HTMLParser(htmlText, handler = {
152         chars: (text) => {
153           text = decodeHTML(text);
154           plainText += text;
155           endIndex += text.length;
156           if (endIndex - beginIndex) {
157             if (prevTextRun && prevTextRun.textFormat.equals(textFormat)) {
158               prevTextRun.endIndex = endIndex;
159             } else {
160               prevTextRun = new this.sec.flash.text.TextRun(beginIndex, endIndex, textFormat);
161               textRuns.push(prevTextRun);
162             }
163             beginIndex = endIndex;
164           }
165         },
166         start: (tagName, attributes) => {
167           var hasStyle = false;
168           if (styleSheet) {
169             hasStyle = styleSheet.hasStyle(tagName);
170             if (hasStyle) {
171               stack.push(textFormat);
172               textFormat = textFormat.clone();
173               styleSheet.applyStyle(textFormat, tagName);
174             }
175           }
177           switch (tagName) {
178             case 'a':
179               stack.push(textFormat);
180               somewhatImplemented('<a/>');
181               var target = attributes.target || textFormat.target;
182               var url = attributes.url || textFormat.url;
183               if (target !== textFormat.target || url !== textFormat.url) {
184                 if (!hasStyle) {
185                   textFormat = textFormat.clone();
186                 }
187                 textFormat.target = target;
188                 textFormat.url = url;
189               }
190               break;
191             case 'b':
192               stack.push(textFormat);
193               if (!textFormat.bold) {
194                 if (!hasStyle) {
195                   textFormat = textFormat.clone();
196                 }
197                 textFormat.bold = true;
198               }
199               break;
200             case 'font':
201               stack.push(textFormat);
202               var color = ColorUtilities.isValidHexColor(attributes.color) ? ColorUtilities.hexToRGB(attributes.color) : textFormat.color;
203               var font = attributes.face || textFormat.font;
204               var size = isNaN(attributes.size) ? textFormat.size : +attributes.size;
205               var letterSpacing = isNaN(attributes.letterspacing) ? textFormat.letterSpacing : +attributes.letterspacing;
206               var kerning = isNaN(attributes.kerning) ? textFormat.kerning : +attributes.kerning;
207               if (color !== textFormat.color ||
208                   font !== textFormat.font ||
209                   size !== textFormat.size ||
210                   letterSpacing !== textFormat.letterSpacing ||
211                   kerning !== textFormat.kerning)
212               {
213                 if (!hasStyle) {
214                   textFormat = textFormat.clone();
215                 }
216                 textFormat.color = color;
217                 textFormat.font = font;
218                 textFormat.size = size;
219                 textFormat.letterSpacing = letterSpacing;
220                 textFormat.kerning = kerning;
221               }
222               break;
223             case 'img':
224               notImplemented('<img/>');
225               break;
226             case 'i':
227               stack.push(textFormat);
228               if (!textFormat.italic) {
229                 if (!hasStyle) {
230                   textFormat = textFormat.clone();
231                 }
232                 textFormat.italic = true;
233               }
234               break;
235             case 'li':
236               stack.push(textFormat);
237               if (!textFormat.bullet) {
238                 if (!hasStyle) {
239                   textFormat = textFormat.clone();
240                 }
241                 textFormat.bullet = true;
242               }
243               if (plainText[plainText.length - 1] === '\r') {
244                 break;
245               }
246             case 'br':
247             case 'sbr':
248               if (multiline) {
249                 handler.chars('\r');
250               }
251               break;
252             case 'span':
253             case 'p':
254               var hasClassStyle = false;
255               stack.push(textFormat);
257               if (styleSheet && attributes.class) {
258                 var cssClass = '.' + attributes.class;
259                 hasClassStyle = styleSheet.hasStyle(cssClass);
260                 if (hasClassStyle) {
261                   if (!hasStyle) {
262                     textFormat = textFormat.clone();
263                   }
264                   styleSheet.applyStyle(textFormat, cssClass);
265                 }
266               }
268               if (tagName === 'span') {
269                 break;
270               }
272               var align = attributes.align;
273               if (flash.text.TextFormatAlign.toNumber(align) > -1 && align !== textFormat.align) {
274                 if (!(hasStyle || hasClassStyle)) {
275                   textFormat = textFormat.clone();
276                 }
277                 textFormat.align = align;
278               }
279               break;
280             case 'textformat':
281               stack.push(textFormat);
282               var blockIndent = isNaN(attributes.blockindent) ? textFormat.blockIndent : +attributes.blockindent;
283               var indent      = isNaN(attributes.indent)      ? textFormat.indent      : +attributes.indent;
284               var leading     = isNaN(attributes.leading)     ? textFormat.leading     : +attributes.leading;
285               var leftMargin  = isNaN(attributes.leftmargin)  ? textFormat.leftMargin  : +attributes.leftmargin;
286               var rightMargin = isNaN(attributes.rightmargin) ? textFormat.rightMargin : +attributes.rightmargin;
287               //var tabStops = attributes.tabstops || textFormat.tabStops;
288               if (blockIndent !== textFormat.blockIndent ||
289                   indent !== textFormat.indent ||
290                   leading !== textFormat.leading ||
291                   leftMargin !== textFormat.leftMargin ||
292                   rightMargin !== textFormat.rightMargin /*||
293                   tabStops !== textFormat.tabStops*/)
294               {
295                 if (!hasStyle) {
296                   textFormat = textFormat.clone();
297                 }
298                 textFormat.blockIndent = blockIndent;
299                 textFormat.indent = indent;
300                 textFormat.leading = leading;
301                 textFormat.leftMargin = leftMargin;
302                 textFormat.rightMargin = rightMargin;
303                 //textFormat.tabStops = tabStops;
304               }
305               break;
306             case 'u':
307               stack.push(textFormat);
308               if (!textFormat.underline) {
309                 if (!hasStyle) {
310                   textFormat = textFormat.clone();
311                 }
312                 textFormat.underline = true;
313               }
314               break;
315           }
316         },
317         end: (tagName) => {
318           switch (tagName) {
319             case 'li':
320             case 'p':
321               if (multiline) {
322                 handler.chars('\r');
323               }
324             case 'a':
325             case 'b':
326             case 'font':
327             case 'i':
328             case 'textformat':
329             case 'u':
330               textFormat = stack.pop();
331               if (styleSheet && styleSheet.hasStyle(tagName)) {
332                 textFormat = stack.pop();
333               }
334           }
335         }
336       });
338       this._plainText = plainText;
339       this._serializeTextRuns();
340     }
342     get plainText(): string {
343       return this._plainText;
344     }
346     set plainText(value: string) {
347       this._plainText = value.split('\n').join('\r');
348       this.textRuns.length = 0;
349       if (value) {
350         var textRun = new this.sec.flash.text.TextRun(0, value.length, this.defaultTextFormat);
351         this.textRuns[0] = textRun;
352       }
353       this._serializeTextRuns();
354     }
356     get bounds(): Bounds {
357       return this._bounds;
358     }
360     set bounds(bounds: Bounds) {
361       this._bounds.copyFrom(bounds);
362       this.flags |= TextContentFlags.DirtyBounds;
363     }
365     get autoSize(): number {
366       return this._autoSize;
367     }
369     set autoSize(value: number) {
370       if (value === this._autoSize) {
371         return;
372       }
373       this._autoSize = value;
374       if (this._plainText) {
375         this.flags |= TextContentFlags.DirtyFlow;
376       }
377     }
379     get wordWrap(): boolean {
380       return this._wordWrap;
381     }
383     set wordWrap(value: boolean) {
384       if (value === this._wordWrap) {
385         return;
386       }
387       this._wordWrap = value;
388       if (this._plainText) {
389         this.flags |= TextContentFlags.DirtyFlow;
390       }
391     }
393     get scrollV(): number {
394       return this._scrollV;
395     }
397     set scrollV(value: number) {
398       if (value === this._scrollV) {
399         return;
400       }
401       this._scrollV = value;
402       if (this._plainText) {
403         this.flags |= TextContentFlags.DirtyFlow;
404       }
405     }
407     get scrollH(): number {
408       return this._scrollH;
409     }
411     set scrollH(value: number) {
412       if (value === this._scrollH) {
413         return;
414       }
415       this._scrollH = value;
416       if (this._plainText) {
417         this.flags |= TextContentFlags.DirtyFlow;
418       }
419     }
421     get backgroundColor(): number {
422       return this._backgroundColor;
423     }
425     set backgroundColor(value: number) {
426       if (value === this._backgroundColor) {
427         return;
428       }
429       this._backgroundColor = value;
430       this.flags |= TextContentFlags.DirtyStyle;
431     }
433     get borderColor(): number {
434       return this._borderColor;
435     }
437     set borderColor(value: number) {
438       if (value === this._borderColor) {
439         return;
440       }
441       this._borderColor = value;
442       this.flags |= TextContentFlags.DirtyStyle;
443     }
445     private _serializeTextRuns() {
446       var textRuns = this.textRuns;
447       this.textRunData.clear();
448       for (var i = 0; i < textRuns.length; i++) {
449         this._writeTextRun(textRuns[i]);
450       }
451       this.flags |= TextContentFlags.DirtyContent;
452     }
454     private _writeTextRun(textRun: flash.text.TextRun) {
455       var textRunData = this.textRunData;
457       textRunData.writeInt(textRun.beginIndex);
458       textRunData.writeInt(textRun.endIndex);
460       var textFormat = textRun.textFormat;
462       var size = +textFormat.size;
463       textRunData.writeInt(size);
465       var fontClass = this.sec.flash.text.Font.axClass;
466       var font = fontClass.getByNameAndStyle(textFormat.font, textFormat.style) ||
467                  fontClass.getDefaultFont();
468       if (font.fontType === flash.text.FontType.EMBEDDED) {
469         textRunData.writeUTF('swffont' + font._id);
470       } else {
471         textRunData.writeUTF(font._fontFamily);
472       }
473       textRunData.writeInt(altTieBreakRound(font.ascent * size, true));
474       textRunData.writeInt(altTieBreakRound(font.descent * size, false));
475       textRunData.writeInt(textFormat.leading === null ? font.leading * size : +textFormat.leading);
476       // For embedded fonts, always set bold and italic to false. They're fully identified by name.
477       var bold: boolean = false;
478       var italic: boolean = false;
479       if (font.fontType === flash.text.FontType.DEVICE) {
480         if (textFormat.bold === null) {
481           bold = font.fontStyle === flash.text.FontStyle.BOLD ||
482                  font.fontType === flash.text.FontStyle.BOLD_ITALIC;
483         } else {
484           bold = !!textFormat.bold;
485         }
486         if (textFormat.italic === null) {
487           italic = font.fontStyle === flash.text.FontStyle.ITALIC ||
488                    font.fontType === flash.text.FontStyle.BOLD_ITALIC;
489         } else {
490           italic = !!textFormat.italic;
491         }
492       }
493       textRunData.writeBoolean(bold);
494       textRunData.writeBoolean(italic);
496       textRunData.writeInt(+textFormat.color);
497       textRunData.writeInt(flash.text.TextFormatAlign.toNumber(textFormat.align));
498       textRunData.writeBoolean(!!textFormat.bullet);
499       //textRunData.writeInt(textFormat.display);
500       textRunData.writeInt(+textFormat.indent);
501       //textRunData.writeInt(textFormat.blockIndent);
502       textRunData.writeInt(+textFormat.kerning);
503       textRunData.writeInt(+textFormat.leftMargin);
504       textRunData.writeInt(+textFormat.letterSpacing);
505       textRunData.writeInt(+textFormat.rightMargin);
506       //textRunData.writeInt(textFormat.tabStops);
507       textRunData.writeBoolean(!!textFormat.underline);
508     }
510     appendText(newText: string, format?: flash.text.TextFormat) {
511       if (!format) {
512         format = this.defaultTextFormat;
513       }
514       var plainText = this._plainText;
515       var newRun = new this.sec.flash.text.TextRun(plainText.length,
516                                                    plainText.length + newText.length, format);
517       this._plainText = plainText + newText;
518       this.textRuns.push(newRun);
519       this._writeTextRun(newRun);
520     }
522     prependText(newText: string, format?: flash.text.TextFormat) {
523       if (!format) {
524         format = this.defaultTextFormat;
525       }
526       var plainText = this._plainText;
527       this._plainText = newText + plainText;
528       var textRuns = this.textRuns;
529       var shift = newText.length;
530       for (var i = 0; i < textRuns.length; i++) {
531         var run = textRuns[i];
532         run.beginIndex += shift;
533         run.endIndex += shift;
534       }
535       textRuns.unshift(
536         new this.sec.flash.text.TextRun(0, shift, format)
537       );
538       this._serializeTextRuns();
539     }
541     replaceText(beginIndex: number, endIndex: number, newText: string, format?: flash.text.TextFormat) {
542       if (endIndex < beginIndex || !newText) {
543         return;
544       }
546       if (endIndex === 0) {
547         // Insert text at the beginning.
548         this.prependText(newText, format);
549         return;
550       }
552       var plainText = this._plainText;
554       // When inserting text to the end, we can simply add a new text run without changing any
555       // existing ones.
556       if (beginIndex >= plainText.length) {
557         this.appendText(newText, format);
558         return;
559       }
561       var defaultTextFormat = this.defaultTextFormat;
563       // A text format used for new text runs will have unset properties merged in from the default
564       // text format.
565       var newFormat = defaultTextFormat;
566       if (format) {
567         newFormat = newFormat.clone();
568         newFormat.merge(format);
569       }
571       // If replacing the whole text, just regenerate runs by setting plainText.
572       if (beginIndex <= 0 && endIndex >= plainText.length) {
573         if (format) {
574           // Temporarily set the passed text format as default.
575           this.defaultTextFormat = newFormat;
576           this.plainText = newText;
577           // Restore the original default when finished.
578           this.defaultTextFormat = defaultTextFormat;
579         } else {
580           this.plainText = newText;
581         }
582         return;
583       }
585       var textRuns = this.textRuns;
586       var newTextRuns: flash.text.TextRun[] = [];
587       var newEndIndex = beginIndex + newText.length;
588       var shift = newEndIndex - endIndex;
589       for (var i = 0; i < textRuns.length; i++) {
590         var run = textRuns[i];
591         var isLast = i >= textRuns.length - 1;
592         if (beginIndex < run.endIndex) {
593           // Skip all following steps (including adding the current run to the new list of runs) if
594           // the inserted text overlaps the current run, which is not the last one.
595           if (!isLast && beginIndex <= run.beginIndex && newEndIndex >= run.endIndex) {
596             continue;
597           }
598           var containsBeginIndex = run.containsIndex(beginIndex);
599           var containsEndIndex = run.containsIndex(endIndex) ||
600                                  (isLast && endIndex >= run.endIndex);
601           if (containsBeginIndex && containsEndIndex) {
602             // The current run spans over the inserted text.
603             if (format) {
604               // Split up the current run.
605               var clone = run.clone();
606               clone.endIndex = beginIndex;
607               newTextRuns.push(clone);
608               i--;
609               run.beginIndex = beginIndex + 1;
610               continue;
611             }
612           } else if (containsBeginIndex) {
613             // Run is intersecting on the left. Adjust its length.
614             run.endIndex = beginIndex;
615           } else if (containsEndIndex) {
616             // If a a text format was passed, a new run needs to be inserted.
617             if (format) {
618               newTextRuns.push(
619                 new this.sec.flash.text.TextRun(beginIndex, newEndIndex, newFormat)
620               );
621               run.beginIndex = newEndIndex;
622             } else {
623               // Otherwise make the current run span over the inserted text.
624               run.beginIndex = beginIndex;
625               run.endIndex += shift;
626             }
627           } else {
628             // No intersection, shift entire run to the right.
629             run.beginIndex += shift;
630             run.endIndex += shift;
631           }
632         }
633         // Ignore empty runs.
634         if (run.endIndex > run.beginIndex) {
635           newTextRuns.push(run);
636         }
637       }
639       this._plainText = plainText.substring(0, beginIndex) + newText + plainText.substring(endIndex);
640       this.textRuns = newTextRuns;
641       this._serializeTextRuns();
642     }
643   }