2 * Copyright 2014 Mozilla Foundation
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
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 {
30 DirtyContent = 0x0002,
33 Dirty = DirtyBounds | DirtyContent | DirtyStyle | DirtyFlow
36 var _decodeHTMLMap = {
46 * Decodes strings of the format:
54 * This is complete enough to handle encoded HTML produced by the Flash IDE.
56 function decodeHTML(s: string): string {
58 for (var i = 0; i < s.length; i++) {
63 // Look for the first '&' or ';', both of these can terminate
64 // the current char code.
65 var j = StringUtilities.indexOfAny(s, ['&', ';'], i + 1);
67 var v = s.substring(i + 1, j);
68 if (v.length > 1 && v.charAt(0) === "#") {
70 if (v.length > 2 && v.charAt(1) === "x") {
71 n = parseInt(v.substring(1));
73 n = parseInt(v.substring(2), 16);
75 r += String.fromCharCode(n);
77 if (_decodeHTMLMap[v] !== undefined) {
78 r += _decodeHTMLMap[v];
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];
100 export class TextContent implements Shumway.Remoting.IRemotable {
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;
114 defaultTextFormat: flash.text.TextFormat;
115 textRuns: flash.text.TextRun[];
116 textRunData: DataBuffer;
117 matrix: flash.geom.Matrix;
120 constructor(sec: ISecurityDomain, defaultTextFormat?: flash.text.TextFormat) {
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;
128 this._wordWrap = false;
131 this.flags = TextContentFlags.None;
132 this.defaultTextFormat = defaultTextFormat || new sec.flash.text.TextFormat();
134 this.textRunData = new DataBuffer();
139 parseHtml(htmlText: string, styleSheet: flash.text.StyleSheet, multiline: boolean) {
141 var textRuns = this.textRuns;
146 var textFormat = this.defaultTextFormat.clone();
147 var prevTextRun: flash.text.TextRun = null;
150 var handler: HTMLParserHandler;
151 Shumway.HTMLParser(htmlText, handler = {
153 text = decodeHTML(text);
155 endIndex += text.length;
156 if (endIndex - beginIndex) {
157 if (prevTextRun && prevTextRun.textFormat.equals(textFormat)) {
158 prevTextRun.endIndex = endIndex;
160 prevTextRun = new this.sec.flash.text.TextRun(beginIndex, endIndex, textFormat);
161 textRuns.push(prevTextRun);
163 beginIndex = endIndex;
166 start: (tagName, attributes) => {
167 var hasStyle = false;
169 hasStyle = styleSheet.hasStyle(tagName);
171 stack.push(textFormat);
172 textFormat = textFormat.clone();
173 styleSheet.applyStyle(textFormat, tagName);
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) {
185 textFormat = textFormat.clone();
187 textFormat.target = target;
188 textFormat.url = url;
192 stack.push(textFormat);
193 if (!textFormat.bold) {
195 textFormat = textFormat.clone();
197 textFormat.bold = true;
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)
214 textFormat = textFormat.clone();
216 textFormat.color = color;
217 textFormat.font = font;
218 textFormat.size = size;
219 textFormat.letterSpacing = letterSpacing;
220 textFormat.kerning = kerning;
224 notImplemented('<img/>');
227 stack.push(textFormat);
228 if (!textFormat.italic) {
230 textFormat = textFormat.clone();
232 textFormat.italic = true;
236 stack.push(textFormat);
237 if (!textFormat.bullet) {
239 textFormat = textFormat.clone();
241 textFormat.bullet = true;
243 if (plainText[plainText.length - 1] === '\r') {
254 var hasClassStyle = false;
255 stack.push(textFormat);
257 if (styleSheet && attributes.class) {
258 var cssClass = '.' + attributes.class;
259 hasClassStyle = styleSheet.hasStyle(cssClass);
262 textFormat = textFormat.clone();
264 styleSheet.applyStyle(textFormat, cssClass);
268 if (tagName === 'span') {
272 var align = attributes.align;
273 if (flash.text.TextFormatAlign.toNumber(align) > -1 && align !== textFormat.align) {
274 if (!(hasStyle || hasClassStyle)) {
275 textFormat = textFormat.clone();
277 textFormat.align = align;
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*/)
296 textFormat = textFormat.clone();
298 textFormat.blockIndent = blockIndent;
299 textFormat.indent = indent;
300 textFormat.leading = leading;
301 textFormat.leftMargin = leftMargin;
302 textFormat.rightMargin = rightMargin;
303 //textFormat.tabStops = tabStops;
307 stack.push(textFormat);
308 if (!textFormat.underline) {
310 textFormat = textFormat.clone();
312 textFormat.underline = true;
330 textFormat = stack.pop();
331 if (styleSheet && styleSheet.hasStyle(tagName)) {
332 textFormat = stack.pop();
338 this._plainText = plainText;
339 this._serializeTextRuns();
342 get plainText(): string {
343 return this._plainText;
346 set plainText(value: string) {
347 this._plainText = value.split('\n').join('\r');
348 this.textRuns.length = 0;
350 var textRun = new this.sec.flash.text.TextRun(0, value.length, this.defaultTextFormat);
351 this.textRuns[0] = textRun;
353 this._serializeTextRuns();
356 get bounds(): Bounds {
360 set bounds(bounds: Bounds) {
361 this._bounds.copyFrom(bounds);
362 this.flags |= TextContentFlags.DirtyBounds;
365 get autoSize(): number {
366 return this._autoSize;
369 set autoSize(value: number) {
370 if (value === this._autoSize) {
373 this._autoSize = value;
374 if (this._plainText) {
375 this.flags |= TextContentFlags.DirtyFlow;
379 get wordWrap(): boolean {
380 return this._wordWrap;
383 set wordWrap(value: boolean) {
384 if (value === this._wordWrap) {
387 this._wordWrap = value;
388 if (this._plainText) {
389 this.flags |= TextContentFlags.DirtyFlow;
393 get scrollV(): number {
394 return this._scrollV;
397 set scrollV(value: number) {
398 if (value === this._scrollV) {
401 this._scrollV = value;
402 if (this._plainText) {
403 this.flags |= TextContentFlags.DirtyFlow;
407 get scrollH(): number {
408 return this._scrollH;
411 set scrollH(value: number) {
412 if (value === this._scrollH) {
415 this._scrollH = value;
416 if (this._plainText) {
417 this.flags |= TextContentFlags.DirtyFlow;
421 get backgroundColor(): number {
422 return this._backgroundColor;
425 set backgroundColor(value: number) {
426 if (value === this._backgroundColor) {
429 this._backgroundColor = value;
430 this.flags |= TextContentFlags.DirtyStyle;
433 get borderColor(): number {
434 return this._borderColor;
437 set borderColor(value: number) {
438 if (value === this._borderColor) {
441 this._borderColor = value;
442 this.flags |= TextContentFlags.DirtyStyle;
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]);
451 this.flags |= TextContentFlags.DirtyContent;
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);
471 textRunData.writeUTF(font._fontFamily);
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;
484 bold = !!textFormat.bold;
486 if (textFormat.italic === null) {
487 italic = font.fontStyle === flash.text.FontStyle.ITALIC ||
488 font.fontType === flash.text.FontStyle.BOLD_ITALIC;
490 italic = !!textFormat.italic;
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);
510 appendText(newText: string, format?: flash.text.TextFormat) {
512 format = this.defaultTextFormat;
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);
522 prependText(newText: string, format?: flash.text.TextFormat) {
524 format = this.defaultTextFormat;
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;
536 new this.sec.flash.text.TextRun(0, shift, format)
538 this._serializeTextRuns();
541 replaceText(beginIndex: number, endIndex: number, newText: string, format?: flash.text.TextFormat) {
542 if (endIndex < beginIndex || !newText) {
546 if (endIndex === 0) {
547 // Insert text at the beginning.
548 this.prependText(newText, format);
552 var plainText = this._plainText;
554 // When inserting text to the end, we can simply add a new text run without changing any
556 if (beginIndex >= plainText.length) {
557 this.appendText(newText, format);
561 var defaultTextFormat = this.defaultTextFormat;
563 // A text format used for new text runs will have unset properties merged in from the default
565 var newFormat = defaultTextFormat;
567 newFormat = newFormat.clone();
568 newFormat.merge(format);
571 // If replacing the whole text, just regenerate runs by setting plainText.
572 if (beginIndex <= 0 && endIndex >= plainText.length) {
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;
580 this.plainText = newText;
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) {
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.
604 // Split up the current run.
605 var clone = run.clone();
606 clone.endIndex = beginIndex;
607 newTextRuns.push(clone);
609 run.beginIndex = beginIndex + 1;
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.
619 new this.sec.flash.text.TextRun(beginIndex, newEndIndex, newFormat)
621 run.beginIndex = newEndIndex;
623 // Otherwise make the current run span over the inserted text.
624 run.beginIndex = beginIndex;
625 run.endIndex += shift;
628 // No intersection, shift entire run to the right.
629 run.beginIndex += shift;
630 run.endIndex += shift;
633 // Ignore empty runs.
634 if (run.endIndex > run.beginIndex) {
635 newTextRuns.push(run);
639 this._plainText = plainText.substring(0, beginIndex) + newText + plainText.substring(endIndex);
640 this.textRuns = newTextRuns;
641 this._serializeTextRuns();