| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // 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. |
| 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().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</html> |
| * 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 |
| * <p>p element without end tag; closed by next p start tag |
| * <p>closes previous p element with missing end tag</p> |
| * |
| * b) end-tag for an ancestor element |
| * <html><p>p element without end tag; closed by next end tag of an ancestor</html> |
| * TextField dialect: This is true for all elements. |
| * |
| * 3) End tags are forbidden for some elements |
| * <br> and <br/> are valid, but <br></br> is not |
| * TextField dialect: Does not apply. |
| * |
| * 4) Element and attribute names may use any case |
| * <P ALign="left"></p> |
| * |
| * 5) Attribute values may be unquoted |
| * <p align=left/> |
| * |
| * 6) Boolean attributed may assume a minimized form |
| * <p selected/> is equivalent to <p selected="selected"/> |
| * @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 < |
| // 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 "<" |
| 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 |
| } |
| } |
| |