textLayout/src/flashx/textLayout/conversion/TextFieldHtmlImporter.as (1,269 lines of code) (raw):

//////////////////////////////////////////////////////////////////////////////// // // 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 } }