blob: 34cc5e7b4e3c48a8399b050dca515d7dcb396e00 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package flashx.textLayout.conversion
{
import flash.system.System;
import flash.text.engine.Kerning;
import flash.utils.Dictionary;
import flashx.textLayout.debug.assert;
import flashx.textLayout.elements.BreakElement;
import flashx.textLayout.elements.Configuration;
import flashx.textLayout.elements.DivElement;
import flashx.textLayout.elements.FlowElement;
import flashx.textLayout.elements.FlowGroupElement;
import flashx.textLayout.elements.FlowLeafElement;
import flashx.textLayout.elements.GlobalSettings;
import flashx.textLayout.elements.IConfiguration;
import flashx.textLayout.elements.InlineGraphicElement;
import flashx.textLayout.elements.LinkElement;
import flashx.textLayout.elements.ListElement;
import flashx.textLayout.elements.ListItemElement;
import flashx.textLayout.elements.ParagraphElement;
import flashx.textLayout.elements.SpanElement;
import flashx.textLayout.elements.SubParagraphGroupElement;
import flashx.textLayout.elements.SubParagraphGroupElementBase;
import flashx.textLayout.elements.TabElement;
import flashx.textLayout.elements.TextFlow;
import flashx.textLayout.formats.Float;
import flashx.textLayout.formats.ITextLayoutFormat;
import flashx.textLayout.formats.LeadingModel;
import flashx.textLayout.formats.ListMarkerFormat;
import flashx.textLayout.formats.ListStyleType;
import flashx.textLayout.formats.TextLayoutFormat;
import flashx.textLayout.property.Property;
import flashx.textLayout.tlf_internal;
use namespace tlf_internal;
[ExcludeClass]
/**
* @private
* TextFieldHtmlImporter converts from HTML to TextLayout data structures
*/
public class TextFieldHtmlImporter extends BaseTextLayoutImporter implements IHTMLImporter
{
// TLF formats to which <font/> attributes map directly
/** @private */
static tlf_internal const _fontDescription:Object = {
color:TextLayoutFormat.colorProperty,
trackingRight:TextLayoutFormat.trackingRightProperty,
fontFamily:TextLayoutFormat.fontFamilyProperty
};
// <font/> attributes that require custom logic for mapping to TLF formats
/** @private */
static tlf_internal const _fontMiscDescription:Object = {
size : Property.NewStringProperty("size", null, false, null),
kerning : Property.NewStringProperty("kerning", null, false, null)
};
// TLF formats to which <textformat/> attributes map directly
/** @private */
static tlf_internal const _textFormatDescription:Object = {
paragraphStartIndent:TextLayoutFormat.paragraphStartIndentProperty,
paragraphEndIndent:TextLayoutFormat.paragraphEndIndentProperty,
textIndent:TextLayoutFormat.textIndentProperty,
lineHeight:TextLayoutFormat.lineHeightProperty,
tabStops:TextLayoutFormat.tabStopsProperty
};
// <textformat/> attributes that require custom logic for mapping to TLF formats
/** @private */
static tlf_internal const _textFormatMiscDescription:Object = {
blockIndent : Property.NewStringProperty("blockIndent", null, false, null)
};
/** @private */
static tlf_internal const _paragraphFormatDescription:Object = {
textAlign:TextLayoutFormat.textAlignProperty
};
/** @private */
static tlf_internal const _linkHrefDescription:Object = {
href : Property.NewStringProperty("href", null, false, null)
};
/** @private */
static tlf_internal const _linkTargetDescription:Object = {
target : Property.NewStringProperty("target", null, false, null)
};
/** @private */
static tlf_internal const _imageDescription:Object = {
height : InlineGraphicElement.heightPropertyDefinition,
width : InlineGraphicElement.widthPropertyDefinition
};
// Separate description because id value is case-sensitive unlike others
/** @private */
static tlf_internal const _imageMiscDescription:Object = {
src : Property.NewStringProperty("src", null, false, null),
align : Property.NewStringProperty("align", null, false, null)
};
/** @private Finish defining at run time because it has a member variable "CLASS" which is a reserved keyword. */
static tlf_internal const _classAndIdDescription:Object = {
id : Property.NewStringProperty("ID", null, false, null)
};
// For some reason, the following can't be initialized here
/** @private */
static tlf_internal var _fontImporter:FontImporter;
/** @private */
static tlf_internal var _fontMiscImporter:CaseInsensitiveTLFFormatImporter;
/** @private */
static tlf_internal var _textFormatImporter:TextFormatImporter;
/** @private */
static tlf_internal var _textFormatMiscImporter:CaseInsensitiveTLFFormatImporter;
/** @private */
static tlf_internal var _paragraphFormatImporter:HtmlCustomParaFormatImporter;
/** @private */
static tlf_internal var _linkHrefImporter:CaseInsensitiveTLFFormatImporter;
/** @private */
static tlf_internal var _linkTargetImporter:CaseInsensitiveTLFFormatImporter;
/** @private */
static tlf_internal var _ilgFormatImporter:CaseInsensitiveTLFFormatImporter;
/** @private */
static tlf_internal var _ilgMiscFormatImporter:CaseInsensitiveTLFFormatImporter;
/** @private */
static tlf_internal var _classAndIdImporter:CaseInsensitiveTLFFormatImporter;
// Formats specified by formatting elements in the ancestry of the element being parsed currently
/** @private */
static tlf_internal var _activeFormat:TextLayoutFormat = new TextLayoutFormat(); // to be applied to all flow elements
/** @private */
static tlf_internal var _activeParaFormat:TextLayoutFormat = new TextLayoutFormat(); // to be applied to paras only
/** @private */
static tlf_internal var _activeImpliedParaFormat:TextLayoutFormat = null;
// The basis for relative font size calculation
/** @private */
tlf_internal var _baseFontSize:Number;
/** @private */
tlf_internal static var _htmlImporterConfig:ImportExportConfiguration;
private var _imageSourceResolveFunction:Function;
private var _preserveBodyElement:Boolean = false;
private var _importHtmlElement:Boolean = false;
/** Constructor */
public function TextFieldHtmlImporter()
{
createConfig();
super(null, _htmlImporterConfig);
}
/** @private */
tlf_internal static function createConfig():void
{
if (!_htmlImporterConfig)
{
_htmlImporterConfig = new ImportExportConfiguration();
_htmlImporterConfig.addIEInfo("BR", BreakElement, BaseTextLayoutImporter.parseBreak, null);
_htmlImporterConfig.addIEInfo("P", ParagraphElement, TextFieldHtmlImporter.parsePara, null);
_htmlImporterConfig.addIEInfo("SPAN", SpanElement, TextFieldHtmlImporter.parseSpan, null);
_htmlImporterConfig.addIEInfo("A", LinkElement, TextFieldHtmlImporter.parseLink, null);
_htmlImporterConfig.addIEInfo("IMG", InlineGraphicElement, TextFieldHtmlImporter.parseInlineGraphic, null);
_htmlImporterConfig.addIEInfo("DIV", DivElement, TextFieldHtmlImporter.parseDiv, null);
_htmlImporterConfig.addIEInfo("HTML", null, TextFieldHtmlImporter.parseHtmlElement, null);
_htmlImporterConfig.addIEInfo("BODY", null, TextFieldHtmlImporter.parseBody, null);
// formatting elements
_htmlImporterConfig.addIEInfo("FONT", null, TextFieldHtmlImporter.parseFont, null);
_htmlImporterConfig.addIEInfo("TEXTFORMAT", null, TextFieldHtmlImporter.parseTextFormat, null);
_htmlImporterConfig.addIEInfo("U", null, TextFieldHtmlImporter.parseUnderline, null);
_htmlImporterConfig.addIEInfo("I", null, TextFieldHtmlImporter.parseItalic, null);
_htmlImporterConfig.addIEInfo("B", null, TextFieldHtmlImporter.parseBold, null);
_htmlImporterConfig.addIEInfo("S", null, TextFieldHtmlImporter.parseStrikeThrough, null);
// list stuff
_htmlImporterConfig.addIEInfo("UL", null, BaseTextLayoutImporter.parseList, null);
_htmlImporterConfig.addIEInfo("OL", null, BaseTextLayoutImporter.parseList, null);
_htmlImporterConfig.addIEInfo("LI", null, TextFieldHtmlImporter.parseListItem, null);
}
// create these here - can't be done above
if (_classAndIdDescription["CLASS"] === undefined)
{
_classAndIdDescription["CLASS"] = Property.NewStringProperty("CLASS", null, false, null);
_paragraphFormatImporter = new HtmlCustomParaFormatImporter(TextLayoutFormat, _paragraphFormatDescription);
_textFormatImporter = new TextFormatImporter(TextLayoutFormat, _textFormatDescription);
_fontImporter = new FontImporter(TextLayoutFormat, _fontDescription);
_fontMiscImporter = new CaseInsensitiveTLFFormatImporter(Dictionary, _fontMiscDescription);
_textFormatMiscImporter = new CaseInsensitiveTLFFormatImporter(Dictionary, _textFormatMiscDescription);
_linkHrefImporter = new CaseInsensitiveTLFFormatImporter(Dictionary,_linkHrefDescription,false);
_linkTargetImporter = new CaseInsensitiveTLFFormatImporter(Dictionary,_linkTargetDescription);
_ilgFormatImporter = new CaseInsensitiveTLFFormatImporter(Dictionary,_imageDescription);
_ilgMiscFormatImporter = new CaseInsensitiveTLFFormatImporter(Dictionary,_imageMiscDescription, false);
_classAndIdImporter = new CaseInsensitiveTLFFormatImporter(Dictionary,_classAndIdDescription);
}
}
/** @copy IHTMLExporter#imageSourceResolveFunction
*
* @playerversion Flash 10.0
* @playerversion AIR 2.0
* @langversion 3.0
*/
public function get imageSourceResolveFunction():Function
{ return _imageSourceResolveFunction; }
public function set imageSourceResolveFunction(resolver:Function):void
{ _imageSourceResolveFunction = resolver; }
/** @copy IHTMLExporter#preserveBodyElement
*
* @playerversion Flash 10.0
* @playerversion AIR 2.0
* @langversion 3.0
*/
public function get preserveBodyElement():Boolean
{ return _preserveBodyElement; }
public function set preserveBodyElement(value:Boolean):void
{ _preserveBodyElement = value; }
/** @copy IHTMLExporter#preserveHTMLElement
*
* @playerversion Flash 10.0
* @playerversion AIR 2.0
* @langversion 3.0
*/
public function get preserveHTMLElement():Boolean
{ return _importHtmlElement; }
public function set preserveHTMLElement(value:Boolean):void
{ _importHtmlElement = value; }
/** Parse and convert input data
*
* @param source - the HTML string
*/
protected override function importFromString(source:String):TextFlow
{
var textFlow:TextFlow;
// Use toXML rather than the XML constructor because the latter expects
// well-formed XML, which source may not be
var xml:XML = toXML(source);
if (xml)
{
textFlow = importFromXML(xml);
if (Configuration.playerEnablesArgoFeatures)
System["disposeXML"](xml);
}
return textFlow;
}
/** Parse and convert input XML data
*/
protected override function importFromXML(xmlSource:XML):TextFlow
{
var textFlow:TextFlow = new TextFlow(_textFlowConfiguration);
// always set html typeName - the convertfromstring to xml code wraps the content in an HTML element. this makes it all match
if (this.preserveHTMLElement)
textFlow.typeName = "html";
// Use font size specified in _textFlowConfiguration.textFlowInitialFormat as the base font size
// If not specified, use 12
_baseFontSize = textFlow.fontSize === undefined ? 12 : textFlow.fontSize;
// Unlike other markup formats, the HTML format for TLF does not have a fixed root XML element.
// <html> and <body> are optional, and flow elements may or may not be encapsulated in formatting
// elements like <i> or <textformat>. Use parseObject to handle any (expected) root element.
CONFIG::debug { assert(xmlSource.name() != null, "Bad XML source"); }
parseObject(xmlSource.name().localName, xmlSource, textFlow);
// If the last para is implied, there is nothing following it that'll trigger a reset.
// For most importers, this is fine (clear will eventually reset it), but the HTML importer has
// some special behavior associated with the reset (replacing BreakElements with para splits).
// Explicitly do so now (must happen before normalization)
resetImpliedPara();
CONFIG::debug { textFlow.debugCheckNormalizeAll() ; }
textFlow.normalize();
textFlow.applyWhiteSpaceCollapse(null);
return textFlow;
}
/** @copy ConverterBase#clear()
* @private
*/
tlf_internal override function clear():void
{
// Reset active formats and base font size
_activeParaFormat.clearStyles();
_activeFormat.clearStyles();
super.clear();
}
/** @private */
tlf_internal override function createImpliedParagraph():ParagraphElement
{
var rslt:ParagraphElement;
var savedActiveFormat:TextLayoutFormat = _activeFormat;
if (_activeImpliedParaFormat)
_activeFormat = _activeImpliedParaFormat;
try
{
rslt = super.createImpliedParagraph();
}
finally
{
_activeFormat = savedActiveFormat;
}
return rslt;
}
/** @private */
public override function createParagraphFromXML(xmlToParse:XML):ParagraphElement
{
var paraElem:ParagraphElement = new ParagraphElement();
// Parse xml attributes for paragraph format
var formatImporters:Array = [_paragraphFormatImporter, _classAndIdImporter];
parseAttributes(xmlToParse, formatImporters);
var paragraphFormat:TextLayoutFormat = new TextLayoutFormat(_paragraphFormatImporter.result as ITextLayoutFormat);
// Apply paragraph format inherited from formatting elements
if (_activeParaFormat)
paragraphFormat.apply(_activeParaFormat);
if (_activeFormat)
paragraphFormat.apply(_activeFormat);
// A <FONT/> that is the only child of a <P/> specifies formats that apply to the paragraph itself
// Otherwise (i.e., if it has siblings), the formats apply to the elements nested within the <FONT/>
// Check for the former case here
var fontFormattingElement:XML = getSingleFontChild (xmlToParse);
if (fontFormattingElement)
paragraphFormat.apply(parseFontAttributes(fontFormattingElement));
if (paragraphFormat.lineHeight !== undefined)
paragraphFormat.leadingModel = LeadingModel.APPROXIMATE_TEXT_FIELD;
paraElem.format = paragraphFormat;
// Use the value of the 'class' attribute (if present) as styleName
paraElem.styleName = _classAndIdImporter.getFormatValue("CLASS");
paraElem.id = _classAndIdImporter.getFormatValue("ID");
return paraElem;
}
/** @private */
static public function parseListItem(importer:TextFieldHtmlImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
if (!(parent is ListElement))
{
var list:ListElement = importer.createListFromXML(null);
importer.addChild(parent,list);
CONFIG::debug { assert(list.parent == parent,"TextFieldHtmlImporter.parseListeItem: Bad call to addChild"); }
parent = list;
}
var listItem:ListItemElement = importer.createListItemFromXML(xmlToParse);
if (importer.addChild(parent, listItem))
{
importer.parseFlowGroupElementChildren(xmlToParse, listItem);
//if parsing an empty list item, create a Paragraph for it.
if (listItem.numChildren == 0)
listItem.addChild(new ParagraphElement());
}
}
/** @private */
public override function createListFromXML(xmlToParse:XML):ListElement // No PMD
{
parseAttributes(xmlToParse, [ _classAndIdImporter ]);
// try and make lists look something like TextField but with a bit more - do ordered lists as well not just bullets
// textField indents lists on the left by 36 pixels.
var list:ListElement = new ListElement();
list.paddingLeft = 36;
var name:String = xmlToParse ? xmlToParse.name().localName : null;
list.listStyleType = name == "OL" ? ListStyleType.DECIMAL : ListStyleType.DISC;
// TextField does equivalent of a 18 pixel start indent but that doesnt look nice with numbered lists.
// default bullet is around 4 pixels. so place the marker 14 pixels from the right side
var lmf:ListMarkerFormat = new ListMarkerFormat();
lmf.paragraphEndIndent = 14;
list.listMarkerFormat = lmf;
list.styleName = _classAndIdImporter.getFormatValue("CLASS");
list.id = _classAndIdImporter.getFormatValue("ID");
return list;
}
/** @private */
public override function createListItemFromXML(xmlToParse:XML):ListItemElement // No PMD
{
parseAttributes(xmlToParse, [ _classAndIdImporter ]);
var listItem:ListItemElement = new ListItemElement();
listItem.styleName = _classAndIdImporter.getFormatValue("CLASS");
listItem.id = _classAndIdImporter.getFormatValue("ID");
return listItem;
}
/** Parse the supplied XML into a paragraph. Parse the <p/> element and its children.
* @private
* @param importer parser object
* @param xmlToParse content to parse
* @param parent the parent for the new content
*/
static public function parsePara(importer:TextFieldHtmlImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
var paraElem:ParagraphElement = importer.createParagraphFromXML(xmlToParse);
if (importer.addChild(parent, paraElem))
{
// Parse children, but if there is only one child, a <FONT/>, skip to *its* children.
// That's because the single <FONT/> chuld has already been parsed in createParagraphFromXML.
var fontFormattingElement:XML = getSingleFontChild (xmlToParse);
parseChildrenUnderNewActiveFormat (importer, fontFormattingElement ? fontFormattingElement : xmlToParse, paraElem, _activeFormat, null);
//if parsing an empty paragraph, create a Span for it.
if (paraElem.numChildren == 0)
paraElem.addChild(importer.createImpliedSpan(""));
}
// Replace break elements with paragraph splits
// This must happen before normalization else BreakElements may merge or become spans
replaceBreakElementsWithParaSplits(paraElem);
}
/** Parse the supplied XML into a DivElement. Parse the <p/> element and its children.
* @private
* @param importer parser object
* @param xmlToParse content to parse
* @param parent the parent for the new content
*/
static public function parseDiv(importer:TextFieldHtmlImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
var elem:FlowGroupElement;
if (parent.canOwnFlowElement(new DivElement()))
elem = importer.createDivFromXML(xmlToParse);
else
{
elem = importer.createSPGEFromXML(xmlToParse);
elem.typeName = "div";
}
importer.addChild(parent,elem);
importer.parseFlowGroupElementChildren(xmlToParse,elem);
}
static public function parseHtmlElement(importer:TextFieldHtmlImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
// skip the element and descent to the children
if (importer.preserveHTMLElement)
{
if (!(parent is TextFlow))
{
var newParent:FlowGroupElement = ((parent is ParagraphElement) || (parent is SubParagraphGroupElementBase)) ? new SubParagraphGroupElement() : new DivElement;
parent.addChild(newParent);
parent = newParent;
}
importer.parseAttributes(xmlToParse, [ _classAndIdImporter ]);
parent.typeName = "html";
parent.styleName = _classAndIdImporter.getFormatValue("CLASS");
parent.id = _classAndIdImporter.getFormatValue("ID");
}
importer.parseFlowGroupElementChildren(xmlToParse, parent, null, true);
}
static public function parseBody(importer:TextFieldHtmlImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
// skip the element and descent to the children
if (importer.preserveBodyElement)
{
var newParent:FlowGroupElement = ((parent is ParagraphElement) || (parent is SubParagraphGroupElementBase)) ? new SubParagraphGroupElement() : new DivElement;
parent.addChild(newParent);
parent = newParent;
importer.parseAttributes(xmlToParse, [ _classAndIdImporter ]);
parent.typeName = "body";
parent.styleName = _classAndIdImporter.getFormatValue("CLASS");
parent.id = _classAndIdImporter.getFormatValue("ID");
}
importer.parseFlowGroupElementChildren(xmlToParse, parent, null, true);
}
public function createDivFromXML(xmlToParse:XML):DivElement
{
parseAttributes(xmlToParse, [ _classAndIdImporter ]);
var divElement:DivElement = new DivElement();
divElement.styleName = _classAndIdImporter.getFormatValue("CLASS");
divElement.id = _classAndIdImporter.getFormatValue("ID");
return divElement;
}
public function createSPGEFromXML(xmlToParse:XML):SubParagraphGroupElement
{
parseAttributes(xmlToParse, [ _classAndIdImporter ]);
var spge:SubParagraphGroupElement = new SubParagraphGroupElement();
spge.styleName = _classAndIdImporter.getFormatValue("CLASS");
spge.id = _classAndIdImporter.getFormatValue("ID");
return spge;
}
/** @private */
protected override function onResetImpliedPara(para:ParagraphElement):void
{
// Replacing break elements with paragraph splits, even for implied paras
replaceBreakElementsWithParaSplits (para);
}
/** If the provided xml has a single child <FONT.../>, get it
* @private
*/
static private function getSingleFontChild (xmlToParse:XML):XML
{
var children:XMLList = xmlToParse.children();
if (children.length() == 1)
{
var child:XML = children[0];
if (child.name() && child.name().localName.toUpperCase() == "FONT")
return child;
}
return null;
}
private function createLinkFromXML(xmlToParse:XML):LinkElement
{
var linkElem:LinkElement = new LinkElement();
var formatImporters:Array = [ _linkHrefImporter, _linkTargetImporter, _classAndIdImporter ];
parseAttributes(xmlToParse, formatImporters);
linkElem.href = _linkHrefImporter.getFormatValue("HREF");
linkElem.target = _linkTargetImporter.getFormatValue("TARGET");
// Handle difference in defaults between TextField and TLF
// target "_self" vs. null (equivalent to "_blank")
if (!linkElem.target)
linkElem.target = "_self";
// Apply active format
linkElem.format = _activeFormat;
linkElem.styleName = _classAndIdImporter.getFormatValue("CLASS");
linkElem.id = _classAndIdImporter.getFormatValue("ID");
return linkElem;
}
/** Parse the supplied XML into a LinkElement. Parse the <a/> element and its children.
* @private
* @param importer parser object
* @param xmlToParse content to parse
* @param parent the parent for the new content
*/
static public function parseLink(importer:TextFieldHtmlImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
var linkElem:LinkElement = importer.createLinkFromXML(xmlToParse);
if (importer.addChild(parent, linkElem))
{
parseChildrenUnderNewActiveFormat (importer, xmlToParse, linkElem, _activeFormat, null);
}
}
/** @private returns a string if its a simple span otherwise null */
tlf_internal static function extractSimpleSpanText(xmlToParse:XML):String
{
var elemList:XMLList = xmlToParse[0].children();
if (elemList.length() == 0)
return "";
if (elemList.length() != 1)
return null;
// sniff the first child and test if its a textelement
for each (var child:XML in elemList)
break;
var elemName:String = child.name() ? child.name().localName : null;
if (elemName != null)
return null;
var rslt:String = child.toString();
return rslt ? rslt : "";
}
/** Static method for constructing a span from XML. Parse the <span> ... </span> tag.
* Insert the new content into its parent
* Note: Differs from BaseTextLayoutImporter.parseSpan in that it allows nested <span/> elements.
* @private
* @param importer parser object
* @param xmlToParse content to parse
* @param parent the parent for the new content
*/
static public function parseSpan(importer:TextFieldHtmlImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
// Use the value of the 'class' attribute (if present) as styleName
importer.parseAttributes(xmlToParse,[_classAndIdImporter]);
// if either class or id is set and its not a "simple" span then we need to create an SPGE and descend
var classFormatValue:* = _classAndIdImporter.getFormatValue("CLASS");
var idFormatValue:* = _classAndIdImporter.getFormatValue("ID");
var simpleSpanText:String = extractSimpleSpanText(xmlToParse);
if (simpleSpanText == null)
{
// if its an interesting span make an SPGE otherwise just parse the children
if (classFormatValue !== undefined || idFormatValue !== undefined || !TextLayoutFormat.isEqual(_activeFormat,TextLayoutFormat.emptyTextLayoutFormat))
{
var spge:SubParagraphGroupElement = new SubParagraphGroupElement();
spge.format = _activeFormat;
spge.styleName = classFormatValue;
spge.id = idFormatValue;
spge.typeName = "span";
importer.addChild(parent,spge);
parent = spge;
}
parseChildrenUnderNewActiveFormat (importer, xmlToParse, parent, _activeFormat, null);
return;
}
var span:SpanElement = new SpanElement();
span.format = _activeFormat;
span.styleName = classFormatValue;
span.id = idFormatValue;
span.text = simpleSpanText;
importer.addChild(parent, span);
}
/** create an implied span with specified text */
public override function createImpliedSpan(text:String):SpanElement
{
var span:SpanElement = super.createImpliedSpan(text);
span.format = _activeFormat;
return span;
}
/** @private */
protected function createInlineGraphicFromXML(xmlToParse:XML):InlineGraphicElement
{
var imgElem:InlineGraphicElement = new InlineGraphicElement();
var formatImporters:Array = [_ilgFormatImporter, _ilgMiscFormatImporter, _classAndIdImporter];
parseAttributes(xmlToParse,formatImporters);
var source:String = _ilgMiscFormatImporter.getFormatValue("SRC");
imgElem.source = _imageSourceResolveFunction != null ? _imageSourceResolveFunction(source) : source;
// if not defined then let InlineGraphic set its own default
imgElem.height = InlineGraphicElement.heightPropertyDefinition.setHelper(imgElem.height,_ilgFormatImporter.getFormatValue("HEIGHT"));
imgElem.width = InlineGraphicElement.heightPropertyDefinition.setHelper(imgElem.width,_ilgFormatImporter.getFormatValue("WIDTH"));
var floatVal:String = _ilgMiscFormatImporter.getFormatValue("ALIGN");
// Handle difference in defaults between TextField and TLF
// float "left" vs. "none"
if (floatVal == Float.LEFT || floatVal == Float.RIGHT)
imgElem.float = floatVal;
// Apply active format
imgElem.format = _activeFormat;
imgElem.id = _classAndIdImporter.getFormatValue("ID");
imgElem.styleName = _classAndIdImporter.getFormatValue("CLASS");
return imgElem;
}
/** Parse the supplied XML into an InlineGraphicElement. Parse the <img/> element.
* @private
* @param importer parser object
* @param xmlToParse content to parse
* @param parent the parent for the new content
*/
static public function parseInlineGraphic(importer:TextFieldHtmlImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
var ilg:InlineGraphicElement = importer.createInlineGraphicFromXML(xmlToParse);
importer.addChild(parent, ilg);
}
/** @private */
public override function createTabFromXML(xmlToParse:XML):TabElement
{
return null; // no tabs in HTML
}
/** Parse the attributes of the <Font/> formatting element and returns the corresponding TLF format
* @private
*/
protected function parseFontAttributes(xmlToParse:XML):ITextLayoutFormat
{
var formatImporters:Array = [_fontImporter, _fontMiscImporter];
parseAttributes(xmlToParse, formatImporters);
var newFormat:TextLayoutFormat = new TextLayoutFormat(_fontImporter.result as ITextLayoutFormat);
var kerning:String = _fontMiscImporter.getFormatValue("KERNING");
if (kerning)
{
var kerningVal:Number = Number(kerning);
newFormat.kerning = kerningVal == 0 ? Kerning.OFF : Kerning.AUTO;
}
var size:String = _fontMiscImporter.getFormatValue("SIZE");
if (size)
{
var sizeVal:Number = TextLayoutFormat.fontSizeProperty.setHelper(NaN, size);
if (!isNaN(sizeVal))
{
if (size.search(/\s*(-|\+)/) != -1) // leading whitespace followed by + or -
sizeVal += _baseFontSize; // implies relative font sizes
newFormat.fontSize = sizeVal;
}
}
return newFormat;
}
/** Parse the <Font/> formatting element
* Calculates the new format to apply to _activeFormat and continues parsing down the hierarchy
* @private
*/
static public function parseFont(importer:TextFieldHtmlImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
var newFormat:ITextLayoutFormat = importer.parseFontAttributes (xmlToParse);
parseChildrenUnderNewActiveFormatWithImpliedParaFormat(importer, xmlToParse, parent, newFormat);
}
/** Parse the <TextFormat> formatting element
* Calculates the new format to apply to _activeParaFormat and continues parsing down the hierarchy
* @private
*/
static public function parseTextFormat(importer:TextFieldHtmlImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
var formatImporters:Array = [_textFormatImporter, _textFormatMiscImporter];
importer.parseAttributes(xmlToParse, formatImporters);
var newFormat:TextLayoutFormat = new TextLayoutFormat(_textFormatImporter.result as ITextLayoutFormat);
var blockIndent:* = _textFormatMiscImporter.getFormatValue("BLOCKINDENT");
if (blockIndent !== undefined)
{
// TODO: Nested <TextFormat/>?
blockIndent = TextLayoutFormat.paragraphStartIndentProperty.setHelper(undefined, blockIndent);
if (blockIndent !== undefined)
{
var blockIndentVal:Number = Number(blockIndent);
newFormat.paragraphStartIndent = newFormat.paragraphStartIndent === undefined ? blockIndentVal : newFormat.paragraphStartIndent + blockIndentVal;
}
}
// lineHeight is the only textformat property that must be copied down into subparagraph elements
var saveLineHeight:* = _activeFormat.lineHeight;
if (parent is ParagraphElement)
{
if (parent.numChildren == 0)
{
// if <textFormat> is the first child then promote all the settings to the Paragraph
var format:TextLayoutFormat = new TextLayoutFormat(parent.format);
format.apply(newFormat);
// same as createParagraphFromXML
if (format.lineHeight !== undefined)
format.leadingModel = LeadingModel.APPROXIMATE_TEXT_FIELD;
parent.format = format;
newFormat.clearStyles();
}
else if (newFormat.lineHeight !== undefined)
_activeFormat.lineHeight = newFormat.lineHeight;
}
parseChildrenUnderNewActiveFormat (importer, xmlToParse, parent, _activeParaFormat, newFormat, true);
_activeFormat.lineHeight = saveLineHeight;
}
/** Parse the <b> formatting element
* Calculates the new format to apply to _activeFormat and continues parsing down the hierarchy
* @private
*/
static public function parseBold(importer:TextFieldHtmlImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
var newFormat:TextLayoutFormat = new TextLayoutFormat();
newFormat.fontWeight = flash.text.engine.FontWeight.BOLD;
parseChildrenUnderNewActiveFormatWithImpliedParaFormat (importer, xmlToParse, parent, newFormat);
}
/** Parse the <i> formatting element
* Calculates the new format to apply to _activeFormat and continues parsing down the hierarchy
* @private
*/
static public function parseItalic(importer:TextFieldHtmlImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
var newFormat:TextLayoutFormat = new TextLayoutFormat();
newFormat.fontStyle = flash.text.engine.FontPosture.ITALIC;
parseChildrenUnderNewActiveFormatWithImpliedParaFormat (importer, xmlToParse, parent, newFormat);
}
/** Parse the <b> formatting element
* Calculates the new format to apply to _activeFormat and continues parsing down the hierarchy
* @private
*/
static public function parseStrikeThrough(importer:TextFieldHtmlImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
var newFormat:TextLayoutFormat = new TextLayoutFormat();
newFormat.lineThrough = true;
parseChildrenUnderNewActiveFormatWithImpliedParaFormat (importer, xmlToParse, parent, newFormat);
}
/** Parse the <u> formatting element
* Calculates the new format to apply to _activeFormat and continues parsing down the hierarchy
* @private
*/
static public function parseUnderline(importer:TextFieldHtmlImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
var newFormat:TextLayoutFormat = new TextLayoutFormat();
newFormat.textDecoration = flashx.textLayout.formats.TextDecoration.UNDERLINE;
parseChildrenUnderNewActiveFormatWithImpliedParaFormat(importer, xmlToParse, parent, newFormat);
}
/** @private */
static protected function parseChildrenUnderNewActiveFormatWithImpliedParaFormat(importer:TextFieldHtmlImporter, xmlToParse:XML, parent:FlowGroupElement, newFormat:ITextLayoutFormat):void
{
var oldActiveImpliedParaFormat:TextLayoutFormat = _activeImpliedParaFormat;
if (_activeImpliedParaFormat == null)
_activeImpliedParaFormat = new TextLayoutFormat(_activeFormat);
try
{
parseChildrenUnderNewActiveFormat(importer, xmlToParse, parent, _activeFormat, newFormat, true);
}
finally
{
_activeImpliedParaFormat = oldActiveImpliedParaFormat;
}
}
/** Updates the current active format and base font size as specified, parses children, and restores the active format and base font size
* There are two different use cases for this method:
* - Parsing children of a formatting XML element like <Font/> or <TextFormat/>. In this case, the TLF format corresponding to the formatting element
* (newFormat) is applied to the currently active format (_activeFormat in the case of <Font/> and _activeParaFormat in the case of <TextFormat/>).
* Children of the formatting element are parsed under this new active format.
* - Parsing children of a flow XML element like <P/> or <A/>. In this case, newFormat is null and the currently active format (_activeFormat) is reset.
* Children of the flow element are parsed under this newly reset format. This is to avoid redundancy (the format is already applied to the flow element).
*
* @param importer parser object
* @param xmlToParse content to parse
* @param parent the parent for the parsed children
* @param currFormat the active format (_activeFormat or _activeParaFormat)
* @param newFormat the format to apply to currFormat while the children are being parsed. If null, currFormat is to be reset.
* @param chainedParent whether parent actually corresponds to xmlToParse or has been chained (such as when xmlToParse is a formatting element). See BaseTextLayoutImporter.parseFlowGroupElementChildren
* @private
*/
static protected function parseChildrenUnderNewActiveFormat (importer:TextFieldHtmlImporter, xmlToParse:XML, parent:FlowGroupElement, currFormat:TextLayoutFormat, newFormat:ITextLayoutFormat, chainedParent:Boolean=false):void
{
// Remember the current state
var restoreBaseFontSize:Number = importer._baseFontSize;
var restoreStyles:Object = Property.shallowCopy(currFormat.getStyles());
if (newFormat)
{
// Update base font size based on the new format
if (newFormat.fontSize !== undefined)
importer._baseFontSize = newFormat.fontSize;
// Apply the new format
currFormat.apply(newFormat);
}
else
{
// Base font size remains unchanged
// Reset the new format
currFormat.clearStyles();
}
try
{
var beforeCount:int = parent.numChildren;
importer.parseFlowGroupElementChildren(xmlToParse, parent, null, chainedParent);
// if nothing was added create something - otherwise this construct fails <p><b/></p>
if (beforeCount == parent.numChildren)
{
var span:SpanElement = importer.createImpliedSpan("");
importer.addChild(parent,span);
}
}
finally
{
// Restore
currFormat.setStyles(restoreStyles,false);
importer._baseFontSize = restoreBaseFontSize;
}
}
/** @private */
protected override function handleUnknownAttribute(elementName:String, propertyName:String):void
{
// A toss-up: report error or ignore? Ignore for now
// If we do end up reporting error, we should add exceptions for documented attributes that we don't handle
// like align on <img/>
}
/** @private */
protected override function handleUnknownElement(name:String, xmlToParse:XML, parent:FlowGroupElement):void
{
var newParent:FlowGroupElement; // scratch
// Not an error (it may be a styling element like <h1/>); continue parsing children
// a couple of cases
// 1) must make a div/spge - if activeFormat or id or stylename is set OR parent is a ListElement (otherwise we wind up trying to put ListItems in a Div which is not supported)
// 2) may make a div/spge - if more than one child is added to parent OR the added child has an id/stylename/typename
// 3) otherwise just set the typeName of the single added child
// Use the value of the 'class' attribute (if present) as styleName
parseAttributes(xmlToParse,[_classAndIdImporter]);
// if either class or id is set and its not a "simple" span then we need to create an SPGE and descend
var classFormatValue:* = _classAndIdImporter.getFormatValue("CLASS");
var idFormatValue:* = _classAndIdImporter.getFormatValue("ID");
if (classFormatValue !== undefined || idFormatValue !== undefined || !TextLayoutFormat.isEqual(_activeFormat,TextLayoutFormat.emptyTextLayoutFormat) || (parent is ListElement))
{
newParent = ((parent is ParagraphElement) || (parent is SubParagraphGroupElementBase)) ? new SubParagraphGroupElement() : new DivElement;
addChild(parent, newParent);
newParent.format = _activeFormat;
newParent.typeName = name.toLowerCase();
newParent.styleName = classFormatValue;
newParent.id = idFormatValue;
parseChildrenUnderNewActiveFormat (this, xmlToParse, newParent, _activeFormat, null);
return;
}
var befNumChildren:int = parent.numChildren;
parseFlowGroupElementChildren(xmlToParse, parent, null, true);
// nothing got added - the custom element will be normalized away so just ignore it
if (befNumChildren == parent.numChildren)
return;
if (befNumChildren+1 == parent.numChildren)
{
// exactly one child was added - just tag it with the typeName if possible
var addedChild:FlowElement = parent.getChildAt(befNumChildren);
if (addedChild.id == null && addedChild.styleName == null && addedChild.typeName == addedChild.defaultTypeName)
{
addedChild.typeName = name.toLowerCase();
return;
}
}
// have to make one - case 1)
newParent = ((parent is ParagraphElement) || (parent is SubParagraphGroupElementBase)) ? new SubParagraphGroupElement() : new DivElement;
newParent.typeName = name.toLowerCase();
newParent.replaceChildren(0,0,parent.mxmlChildren.slice(befNumChildren));
addChild(parent,newParent);
}
tlf_internal override function parseObject(name:String, xmlToParse:XML, parent:FlowGroupElement, exceptionElements:Object=null):void
{
// override to allow upper case tag names
super.parseObject(name.toUpperCase(), xmlToParse, parent, exceptionElements);
}
/** @private */
protected override function checkNamespace(xmlToParse:XML):Boolean
{
/* Ignore namespace */
return true;
}
/** Splits the paragraph wherever a break element occurs and removes the latter
* This is to replicate TextField handling of <br/>: splits the containing paragraph (implied or otherwise)
* The <br/> itself doesn't survive.
* @private
*/
static protected function replaceBreakElementsWithParaSplits(para:ParagraphElement):void
{
// performance: when splitting the paragraph into multiple paragraphs take it out of the TextFlow
var paraArray:Array;
var paraIndex:int;
var paraParent:FlowGroupElement;
// Find each BreakElement and split into a new paragraph
var elem:FlowLeafElement = para.getFirstLeaf();
while (elem)
{
if (!(elem is BreakElement))
{
elem = elem.getNextLeaf(para);
continue;
}
if (!paraArray)
{
paraArray = [ para ];
paraParent = para.parent;
paraIndex = paraParent.getChildIndex(para);
paraParent.removeChildAt(paraIndex);
}
// Split the para right after the BreakElement
//CONFIG::debug { assert(elem.textLength == 1,"Bad TextLength in BreakElement"); }
CONFIG::debug {assert( para.getAbsoluteStart() == 0,"Bad paragraph in replaceBreakElementsWithParaSplits"); }
para = para.splitAtPosition(elem.getAbsoluteStart()+elem.textLength) as ParagraphElement;
paraArray.push(para);
// Remove the BreakElement
elem.parent.removeChild(elem);
// point elem to the first leaf of the new paragraph
elem = para.getFirstLeaf();
}
if (paraArray)
paraParent.replaceChildren(paraIndex,paraIndex,paraArray);
}
/** HTML parsing code
* Uses regular expressions for recognizing constructs like comments, tags etc.
* and a hand-coded parser to recognize the document structure and covert to well-formed xml
* TODO-1/16/2009:List caveats
*/
/* Regex for stuff to be stripped: a comment, processing instruction, or a declaration
*
* <!--.*?--> - comment
* <!-- - start comment
* .*? - anything (including newline character, thanks to the s flag); the ? prevents a greedy match (which could match a --> later in the string)
* --> - end comment
*
* <\?(".*?"|'.*?'|[^>]+)*> - processing instruction
* <\? - start processing instruction
* (".*?"|'.*?'|[^>]+)* - 0 or more of the following (interleaved in any order)
* ".*?" - anything (including >) so long as it is within double quotes; the ? prevents a greedy match (which could match everything until a later " in the string)
* '.*?' - anything (including >) so long as it is within single quotes; the ? prevents a greedy match (which could match everything until a later ' in the string)
* [^>"']+ - one or more characters other than > (because > ends the processing instruction), " (handled above), ' (handled above)
* > - end processing instruction
*
* <!(".*?"|'.*?'|[^>"']+)*> - declaration;
* TODO-1/15/2009:not sure if a declaration can contain > within quotes. Assuming it can, the regex is
* is exactly like processing instruction above except it uses a ! instead of a ?
* @private
*/
/** @private */
tlf_internal static const stripRegex:RegExp = /<!--.*?-->|<\?(".*?"|'.*?'|[^>"']+)*>|<!(".*?"|'.*?'|[^>"']+)*>/sg;
/* Regular expression for an HTML tag
* < - open
*
* (\/?) - start modifier; 0 or 1 occurance of one of /
*
* (\w+) - tag name; 1 or more name characters
*
* ((?:\s+\w+(?:\s*=\s*(?:".*?"|'.*?'|[\w\.]+))?)*) - attributes; 0 or more of the following
* (?:\s+\w+(?:\s*=\s*(?:".*?"|'.*?'|[\w\.]+))?) - attribute; 1 or more space, followed by 1 or more name characters optionally followed by
* \s*=\s*(?:".*?"|'.*?'|[\w\.]+) - attribute value assignment; optional space followed by = followed by more optional space followed by one of
* ".*?" - quoted attribute value (using double quotes); the ? prevents a greedy match (which could match everything until a later " in the string)
* '.*?' - quoted attribute value (using single quotes); the ? prevents a greedy match ((which could match everything until a later ' in the string)
* [\w\.]+ - unquoted attribute value; can only contain name characters or a period
* Note: ?: specifies a non-capturing group (i.e., match won't be recorded or used as a numbered back-reference)
*
* \s* - optional space
*
* (\/?) - end modifer (0 or 1 occurance of /)
*
* > - close
* @private
*/
/** @private */
tlf_internal static const tagRegex:RegExp = /<(\/?)(\w+)((?:\s+\w+(?:\s*=\s*(?:".*?"|'.*?'|[\w\.]+))?)*)\s*(\/?)>/sg;
/** Regular expression for an attribute. Except for grouping differences, this regex is the same as the one that appears in tagRegex
* @private
*/
tlf_internal static const attrRegex:RegExp = /\s+(\w+)(?:\s*=\s*(".*?"|'.*?'|[\w\.]+))?/sg;
/** Wrapper for core HTML parsing code that manages XML settings during the process
* @private
*/
protected function toXML(source:String):XML
{
var xml:XML;
var originalSettings:Object = XML.settings();
try
{
XML.ignoreProcessingInstructions = false;
XML.ignoreWhitespace = false;
xml = toXMLInternal(source);
}
finally
{
XML.setSettings(originalSettings);
}
return xml;
}
/** Convert HTML string to well-formed xml, accounting for the following HTML oddities
*
* 1) Start tags are optional for some elements.
* Optional start tag not specified&lt;/html&gt;
* TextField dialect: This is true for all elements.
*
* 2) End tags are optional for some elements. Elements with missing end tags may be implicitly closed by
* a) start-tag for a peer element
* &lt;p&gt;p element without end tag; closed by next p start tag
* &lt;p&gt;closes previous p element with missing end tag&lt;/p&gt;
*
* b) end-tag for an ancestor element
* &lt;html&gt;&lt;p&gt;p element without end tag; closed by next end tag of an ancestor&lt;/html&gt;
* TextField dialect: This is true for all elements.
*
* 3) End tags are forbidden for some elements
* &lt;br&gt; and &lt;br/&gt; are valid, but &lt;br&gt;&lt;/br&gt; is not
* TextField dialect: Does not apply.
*
* 4) Element and attribute names may use any case
* &lt;P ALign="left"&gt;&lt;/p&gt;
*
* 5) Attribute values may be unquoted
* &lt;p align=left/&gt;
*
* 6) Boolean attributed may assume a minimized form
* &lt;p selected/&gt; is equivalent to &lt;p selected="selected"/&gt;
* @private
*/
protected function toXMLInternal(source:String):XML
{
// Strip out comments, processing instructions and declaratins
source = source.replace(stripRegex, "");
// Parse the source, looking for tags and interleaved text content, creating an XML hierarchy in the process.
// At any given time, there is a chain of 'open' elements corresponding to unclosed tags, the innermost of which is
// tracked by the currElem. Content (element or text) parsed next is added as a child of currElem.
// Root of the XML hierarchy (set to <html/> because the html start tag is optional)
// Note that source may contain an html start tag, in which case we'll end up with two such elements
// This is not quite correct, but handled by the importer
var root:XML = <HTML/>;
var currElem:XML = root;
var lastIndex:int = tagRegex.lastIndex = 0;
var openElemName:String;
do
{
var result:Object = tagRegex.exec(source);
if (!result)
{
// No more tags: add text (starting at search index) as a child of the innermost open element and break out
appendTextChild (currElem, source.substring(lastIndex));
break;
}
if (result.index != lastIndex)
{
// Add text between tags as a child of the innermost open element
appendTextChild (currElem, source.substring(lastIndex, result.index));
}
var tag:String = result[0]; // entire tag
var hasStartModifier:Boolean = (result[1] == "\/"); // modifier after < (/ for end tag)
var name:String = result[2].toUpperCase(); // name; use lower case
var attrs:String = result[3]; // attributes; including whitespace
var hasEndModifier:Boolean = (result[4] == "\/"); // modifier before > (/ for composite start and end tag)
if (!hasStartModifier) // start tag
{
// Special case for implicit closing of <p>
// TODO-12/23/2008: this will need to be handled more generically
if (name == "P" && currElem.name().localName == "P")
currElem = currElem.parent();
// Create an XML element by constructing a tag that can be fed to the XML constructor. Specifically, ensure
// - it is a composite tag (start and end tag together) using the terminating slash shorthand
// - element and attribute names are lower case (this is not required, but doesn't hurt)
// - attribute values are quoted
// - boolean attributes are fully specified (e.g., selected="selected" rather than selected)
tag = "<" + name;
do
{
var innerResult:Object = attrRegex.exec(attrs);
if (!innerResult)
break;
var attrName:String = innerResult[1].toUpperCase();
tag += " " + attrName + "=";
var val:String = innerResult[2] ? innerResult[2] : attrName; /* boolean attribute with implied value equal to attribute name */
var startChar:String = val.charAt(0);
tag += ((startChar == "'" || startChar == "\"") ? val : ("\"" + val + "\""));
} while (true);
tag += "\/>";
// Add the corresponding element as a child of the innermost open element
currElem.appendChild(new XML(tag));
// The new element becomes the innermost open element unless it is already closed because
// - this is a composite start and end tag (i.e., has an end modifier)
// - the start tag itself implies closure
if (!hasEndModifier && !doesStartTagCloseElement(name))
currElem = currElem.children()[currElem.children().length()-1];
}
else // end tag
{
if (hasEndModifier || attrs.length)
{
reportError(GlobalSettings.resourceStringFunction("malformedTag",[tag]));
}
else
{
/*
// Does not apply to TextField dialect
if (isEndTagForbidden(name))
{
xxxreportError("End tag is not allowed for element " + name); NOTE : MAKE A LOCALIZABLE ERROR IF THIS COMES BACK
return null;
}*/
// Move up the chain of open elements looking for a matching name
// The matching element is closed and its parent becomes the innermost open element
// Report error if matching element is not found and it requires a start tag
// All intermediate open elements are also closed provided they don't require end tags
// Report error if an intermediate element requires end tags
var openElem:XML = currElem;
do
{
openElemName = openElem.name().localName;
openElem = openElem.parent();
if (openElemName == name)
{
currElem = openElem;
break;
}
/*
// Does not apply to TextField dialect
else if (isEndTagRequired(openElemName))
{
xxxreportError("Missing end tag for element " + openElemName);
return null;
}*/
if (!openElem)
{
// Does not apply to TextField dialect
/*if (isStartTagRequired(name))
{
xxxreportError("Unexpected end tag " + name);
return null;
}*/
break;
}
}
while (true);
}
}
lastIndex = tagRegex.lastIndex;
if (lastIndex == source.length)
break; // string completely parsed
} while (currElem); // null currElem means <html/> has been closed, so ignore everything else
// No more string to parse, specifically, no more end tags.
// Validate that remaining open elements do not require end tags.
// Does not apply to TextField dialect
/* while (currElem)
{
openElemName = currElem.name().localName;
if (isEndTagRequired(openElemName))
{
xxxreportError("Missing end tag for element " + openElemName);
return null;
}
currElem = currElem.parent();
}*/
return root;
}
/** @private */
protected function doesStartTagCloseElement (tagName:String):Boolean
{
switch (tagName)
{
case "BR":
case "IMG":
return true;
default:
return false;
}
}
/** @private */
tlf_internal static const anyPrintChar:RegExp = /[^\u0009\u000a\u000d\u0020]/g;
/** Adds text as a descendant of the specified XML element. Adds an intermediate <span> element is created if parent is not a <span>
* No action is taken for whitespace-only text
* @private
*/
protected function appendTextChild(parent:XML, text:String):void
{
// No whitespace collapse
// if (text.match(anyPrintChar).length != 0)
{
var parentIsSpan:Boolean = (parent.localName() == "SPAN");
var elemName:String = parentIsSpan ? "DUMMY" : "SPAN";
//var xml:XML = <{elemName}/>;
//xml.appendChild(text);
// The commented-out code above doesn't handle character entities like &lt;
// The following lets the XML constructor handle them
var xmlText:String = "<" + elemName + ">" + text + "<\/" + elemName + ">";
try
{
var xml:XML = new XML(xmlText);
parent.appendChild(parentIsSpan ? xml.children()[0] : xml);
}
catch (e:*)
{
// Report malformed content like "<" instead of "&lt;"
reportError(GlobalSettings.resourceStringFunction("malformedMarkup",[text]));
}
}
}
}
}
import flashx.textLayout.conversion.TLFormatImporter;
/** Specialized to provide case insensitivity (as required by TEXT_FIELD_HTML_FORMAT)
* Keys need to be lower-cased. Values may or may not based on a flag passed to the constructor.
*/
class CaseInsensitiveTLFFormatImporter extends TLFormatImporter
{
private var _convertValuesToLowerCase:Boolean;
public function CaseInsensitiveTLFFormatImporter(classType:Class,description:Object, convertValuesToLowerCase:Boolean=true)
{
_convertValuesToLowerCase = convertValuesToLowerCase;
var lowerCaseDescription:Object = new Object();
for (var prop:Object in description)
{
lowerCaseDescription[prop.toUpperCase()] = description[prop];
}
super(classType, lowerCaseDescription);
}
public override function importOneFormat(key:String,val:String):Boolean
{
return super.importOneFormat(key.toUpperCase(), _convertValuesToLowerCase ? val.toLowerCase() : val);
}
public function getFormatValue (key:String):*
{
return result ? result[key.toUpperCase()] : undefined;
}
}
class HtmlCustomParaFormatImporter extends TLFormatImporter
{
public function HtmlCustomParaFormatImporter(classType:Class,description:Object)
{
super(classType,description);
}
public override function importOneFormat(key:String,val:String):Boolean
{
key = key.toUpperCase();
if (key == "ALIGN")
key = "textAlign";
return super.importOneFormat(key,val.toLowerCase()); // covert val to lowercase because TLF won't accept, say, "RIGHT"
}
}
class TextFormatImporter extends TLFormatImporter
{
public function TextFormatImporter(classType:Class,description:Object)
{
super(classType,description);
}
public override function importOneFormat(key:String,val:String):Boolean
{
key = key.toUpperCase();
if (key == "LEFTMARGIN")
key = "paragraphStartIndent"; // assumed to be left-to-right text since we don't handle DIR attribute
else if (key == "RIGHTMARGIN")
key = "paragraphEndIndent"; // assumed to be left-to-right text since we don't handle DIR attribute
else if (key == "INDENT")
key = "textIndent";
else if (key == "LEADING")
key = "lineHeight";
else if (key == "TABSTOPS")
{
key = "tabStops";
// Comma-delimited in TextField HTML format, space delimited in TLF
val = val.replace(/,/g, ' ');
}
return super.importOneFormat(key,val); // no case-coversion required, values for these formats in TLF are case-insensitive
}
}
class FontImporter extends TLFormatImporter
{
public function FontImporter(classType:Class,description:Object)
{
super(classType,description);
}
public override function importOneFormat(key:String,val:String):Boolean
{
key = key.toUpperCase();
if (key == "LETTERSPACING")
key = "trackingRight";
else if (key == "FACE")
key = "fontFamily";
else if (key == "COLOR")
key = "color";
return super.importOneFormat(key,val); // no case-coversion required, values for these formats in TLF are case-insensitive
}
}