blob: 3eb6ac39cd258422020c5763df8cff08da0b115a [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package flashx.textLayout.conversion
{
import flash.system.System;
import flashx.textLayout.TextLayoutVersion;
import flashx.textLayout.tlf_internal;
import flashx.textLayout.debug.assert;
import flashx.textLayout.elements.BreakElement;
import flashx.textLayout.elements.Configuration;
import flashx.textLayout.elements.ContainerFormattedElement;
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.ListElement;
import flashx.textLayout.elements.ListItemElement;
import flashx.textLayout.elements.ParagraphElement;
import flashx.textLayout.elements.ParagraphFormattedElement;
import flashx.textLayout.elements.SpanElement;
import flashx.textLayout.elements.TabElement;
import flashx.textLayout.elements.TableCellElement;
import flashx.textLayout.elements.TextFlow;
import flashx.textLayout.property.Property;
use namespace tlf_internal;
/**
* BaseTextLayoutImporter is a base class for handling the import/export of TextLayout text
* in the native format.
*/
internal class BaseTextLayoutImporter extends ConverterBase implements ITextImporter
{
private var _ns:Namespace; // namespace of expected in imported/exported content
private var _textFlowNamespace:Namespace; // namespace of the TextFlow element against which the namespaces of the following elements are validated
protected var _config:ImportExportConfiguration;
protected var _textFlowConfiguration:IConfiguration = null;
protected var _importVersion:uint;
// static private const anyPrintChar:RegExp = /[^\s]/g;
// Consider only tab, line feed, carriage return, and space as characters used for pretty-printing.
// While debatable, this is consistent with what CSS does.
static private const anyPrintChar:RegExp = /[^\u0009\u000a\u000d\u0020]/g;
public function BaseTextLayoutImporter(nsValue:Namespace, config:ImportExportConfiguration)
{
_ns = nsValue;
_config = config;
}
tlf_internal override function clear():void
{
super.clear();
_textFlowNamespace = null;
_impliedPara = null;
}
/** @copy ITextImporter#importToFlow()
*/
public function importToFlow(source:Object):TextFlow
{
clear(); // empty results of previous imports
if (throwOnError)
return importToFlowCanThrow(source);
var rslt:TextFlow = null;
var savedErrorHandler:Function = Property.errorHandler;
try
{
Property.errorHandler = importPropertyErrorHandler;
rslt = importToFlowCanThrow(source);
}
catch (e:Error)
{
reportError(e.toString());
}
Property.errorHandler = savedErrorHandler;
return rslt;
}
/** @copy ITextImporter#get configuration()
*/
public function get configuration():IConfiguration
{
return _textFlowConfiguration;
}
public function set configuration(value:IConfiguration):void
{
_textFlowConfiguration = value;
}
/** @private */
protected function importPropertyErrorHandler(p:Property,value:Object):void
{
reportError(Property.createErrorString(p,value));
}
private function importToFlowCanThrow(source:Object):TextFlow
{
if (source is String)
return importFromString(String(source));
else if (source is XML)
return importFromXML(XML(source));
return null;
}
/** Parse and convert input data.
*
* @param source - a string which is in XFL format. String is applied to an XML object then passed
* to importFromXML to be processed. The source must be capable of being cast as an XML
* object (E4X).
*/
protected function importFromString(source:String):TextFlow
{
var originalSettings:Object = XML.settings();
try
{
XML.ignoreProcessingInstructions = false;
XML.ignoreWhitespace = false;
var xmlTree:XML = new XML(source);
}
finally
{
XML.setSettings(originalSettings);
}
var textFlow:TextFlow = importFromXML(xmlTree);
if (Configuration.playerEnablesArgoFeatures)
System["disposeXML"](xmlTree);
return textFlow;
}
/** Parse and convert input data.
*
* xflSource is a XFL formated object which must be capable of being cast as an XML
* object (E4X).
*/
protected function importFromXML(xmlSource:XML):TextFlow
// Parse an XFL hierarchy into a TextFlow, using the geometry supplied by a TextFrame
// to host child containers (e.g. tables). This is the main entry point into this class.
{
return parseContent(xmlSource[0]);
}
// This routine imports a TextFlow
protected function parseContent(rootStory:XML):TextFlow
{
// If the root element isn't a textFlow we know how to parse, keep descending the hierarchy.
var child:XML = rootStory..*::TextFlow[0];
if (child)
return parseTextFlow(this, rootStory);
return null;
}
/** Returns the namespace used in for writing XML/XFL
*
* @return the Namespace being used.
*/
public function get ns(): Namespace
{
return _ns;
}
// Remove double spaces, tabs, and newlines.
// If I have a sequence of different sorts of spaces (e.g., en quad, hair space), would I want them converted down to one space? Probably not.
// For now, u0020 is the only space character we consider for eliminating duplicates, though u00A0 (non-breaking space) is potentially eligible.
static private const dblSpacePattern:RegExp = /[\u0020]{2,}/g;
// Tab, line feed, and carriage return
static private const tabNewLinePattern:RegExp = /[\u0009\u000a\u000d]/g;
protected static function stripWhitespace(insertString:String):String
{
// Replace the newlines and tabs inside the element with spaces.
return insertString.replace(tabNewLinePattern, " ");
}
/** Parse XML and convert to TextFlow.
* @param importer parser object
* @param xmlToParse content to parse
* @param parent always null - this parameter is only provided to match FlowElementInfo.importer signature
* @return TextFlow the new TextFlow created as a result of the parse
*/
static public function parseTextFlow(importer:BaseTextLayoutImporter, xmlToParse:XML, parent:Object=null):TextFlow
{
return importer.createTextFlowFromXML(xmlToParse, null);
}
/**
* Static method to parse the supplied XML into a paragrph.
* Parse the <p ...> tag and it's children.
*
* @param importer parser object
* @param xmlToParse content to parse
* @param parent the parent for the new content
*/
static public function parsePara(importer:BaseTextLayoutImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
var paraElem:ParagraphElement = importer.createParagraphFromXML(xmlToParse);
if (importer.addChild(parent, paraElem))
{
importer.parseFlowGroupElementChildren(xmlToParse, paraElem);
//if parsing an empty paragraph, create a Span for it.
if (paraElem.numChildren == 0)
paraElem.addChild(new SpanElement());
}
}
static protected function copyAllStyleProps(dst:FlowLeafElement,src:FlowLeafElement):void
{
dst.format = src.format;
dst.typeName = src.typeName;
dst.id = src.id;
}
/**
* Static method for constructing a span from XML. Parse the <span> ... </span> tag.
* Insert the span into its parent
*
* @param importer parser object
* @param xmlToParse content to parse
* @param parent the parent for the new content
*/
static public function parseSpan(importer:BaseTextLayoutImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
var firstSpan:SpanElement = importer.createSpanFromXML(xmlToParse);
var elemList:XMLList = xmlToParse[0].children();
if(elemList.length() == 0)
{
// Empty span, but may have formatting, so don't strip it out.
// Note: the normalizer may yet strip it out if it is not the last child, but that's the normalizer's business.
importer.addChild(parent, firstSpan);
return;
}
for each (var child:XML in elemList)
{
var elemName:String = child.name() ? child.name().localName : null;
if (elemName == null) // span text
{
if (firstSpan.parent == null) // hasn't been used yet
{
firstSpan.text = child.toString();
importer.addChild(parent, firstSpan);
}
else
{
var s:SpanElement = new SpanElement(); // No PMD
copyAllStyleProps(s,firstSpan);
s.text = child.toString();
importer.addChild(parent, s);
}
}
else if (elemName == "br")
{
var brElem:BreakElement = importer.createBreakFromXML(child); // may be null
if (brElem)
{
copyAllStyleProps(brElem,firstSpan);
importer.addChild(parent, brElem);
}
else
importer.reportError(GlobalSettings.resourceStringFunction("unexpectedXMLElementInSpan",[ elemName ]));
}
else if (elemName == "tab")
{
var tabElem:TabElement = importer.createTabFromXML(child); // may be null
if (tabElem)
{
copyAllStyleProps(tabElem,firstSpan);
importer.addChild(parent, tabElem);
}
else
importer.reportError(GlobalSettings.resourceStringFunction("unexpectedXMLElementInSpan",[ elemName ]));
}
else
importer.reportError(GlobalSettings.resourceStringFunction("unexpectedXMLElementInSpan",[ elemName ]));
}
}
/**
* Static method for constructing a break element from XML. Validate the <br> ... </br> tag.
* Use "\u2028" as the text; Insert the new element into its parent
*
* @param importer parser object
* @param xmlToParse content to parse
* @param parent the parent for the new content
*/
static public function parseBreak(importer:BaseTextLayoutImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
var breakElem:BreakElement = importer.createBreakFromXML(xmlToParse);
importer.addChild(parent, breakElem);
}
/**
* Static method for constructing a tab element from XML. Validate the <tab> ... </tab> tag.
* Use "\t" as the text; Insert the new element into its parent
*
* @param importer parser object
* @param xmlToParse content to parse
* @param parent the parent for the new content
*/
static public function parseTab(importer:BaseTextLayoutImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
var tabElem:TabElement = importer.createTabFromXML(xmlToParse); // may be null
if (tabElem)
importer.addChild(parent, tabElem);
}
/**
* Static method for constructing a list element from XML.
*
* @param importer parser object
* @param xmlToParse content to parse
* @param parent the parent for the new content
*/
static public function parseList(importer:BaseTextLayoutImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
var listElem:ListElement = importer.createListFromXML(xmlToParse);
if (importer.addChild(parent, listElem))
{
importer.parseFlowGroupElementChildren(xmlToParse, listElem);
}
}
/**
* Static method for constructing a list item from XML.
*
* @param importer parser object
* @param xmlToParse content to parse
* @param parent the parent for the new content
*/
static public function parseListItem(importer:BaseTextLayoutImporter, xmlToParse:XML, parent:FlowGroupElement):void
{
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());
}
}
protected function checkNamespace(xmlToParse:XML):Boolean
{
var elementNS:Namespace = xmlToParse.namespace();
if (!_textFlowNamespace) // Not set yet; must be parsing the TextFlow element
{
// TextFlow element: allow only empty namespace and flow namespace
if (elementNS != ns)
{
reportError(GlobalSettings.resourceStringFunction("unexpectedNamespace", [elementNS.toString()]));
return false;
}
_textFlowNamespace = elementNS;
}
// Other elements: must match the namespace of the TextFlow element
// Specifically, can't be empty unless the TextFlow element's namespace is also empty
else if (elementNS != _textFlowNamespace)
{
reportError(GlobalSettings.resourceStringFunction("unexpectedNamespace", [elementNS.toString()]));
return false;
}
return true;
}
public function parseAttributes(xmlToParse:XML,formatImporters:Array):void
{
var importer:IFormatImporter;
// reset them all
for each (importer in formatImporters)
importer.reset();
if (!xmlToParse)
return;
for each (var item:XML in xmlToParse.attributes())
{
var propertyName:String = item.name().localName;
var propertyValue:String = item.toString();
var imported:Boolean = false;
// Strip out padding properties from XML coming in before TLF 2.0, since they were ignored but are no longer. This preserves the look of the text.
if (xmlToParse.localName() == "TextFlow")
{
if (propertyName == "version") // skip over the version attribute, we've already processed it
continue;
}
else if (_importVersion < TextLayoutVersion.VERSION_2_0 &&
(propertyName == "paddingLeft" || propertyName == "paddingTop" || propertyName == "paddingRight" || propertyName == "paddingBottom"))
continue;
for each (importer in formatImporters)
{
if (importer.importOneFormat(propertyName,propertyValue))
{
imported = true;
break;
}
}
if (!imported) // not a supported attribute
handleUnknownAttribute (xmlToParse.name().localName, propertyName);
}
}
static protected function extractAttributesHelper(curAttrs:Object, importer:TLFormatImporter):Object
{
if (curAttrs == null)
return importer.result;
if (importer.result == null)
return curAttrs;
var workAttrs:Object = new importer.classType(curAttrs);
workAttrs.apply(importer.result);
return workAttrs;
}
/**
* Parse XML and convert to TextFlow.
*
* @param xmlToParse content to parse
* @param textFlow TextFlow we're parsing. If null, create or find a new TextFlow based on XML content
* @return TextFlow the new TextFlow created as a result of the parse
*/
public function createTextFlowFromXML(xmlToParse:XML, newFlow:TextFlow = null):TextFlow // No PMD
{
CONFIG::debug { assert(false,"missing override for createTextFlowFromXML"); }
return null;
}
public function createParagraphFromXML(xmlToParse:XML):ParagraphElement // No PMD
{
CONFIG::debug { assert(false,"missing override for createParagraphFromXML"); }
return null;
}
public function createSpanFromXML(xmlToParse:XML):SpanElement // No PMD
{
CONFIG::debug { assert(false,"missing override for createSpanFromXML"); }
return null;
}
public function createBreakFromXML(xmlToParse:XML):BreakElement
{
parseAttributes(xmlToParse,null); // no attributes allowed - reports errors
return new BreakElement();
}
public function createListFromXML(xmlToParse:XML):ListElement // No PMD
{
CONFIG::debug { assert(false,"missing override for createListFromXML"); }
return null;
}
public function createListItemFromXML(xmlToParse:XML):ListItemElement // No PMD
{
CONFIG::debug { assert(false,"missing override for createListItemFromXML"); }
return null;
}
public function createTabFromXML(xmlToParse:XML):TabElement
{
parseAttributes(xmlToParse,null); // reports errors
return new TabElement();
}
/**
* Parse XML, convert to FlowElements and add to the parent.
*
* @param xmlToParse content to parse
* @param parent the parent for the new content
*/
public function parseFlowChildren(xmlToParse:XML, parent:FlowGroupElement):void
{
parseFlowGroupElementChildren(xmlToParse, parent);
}
/**
* Parse XML, convert to FlowElements and add to the parent.
*
* @param xmlToParse content to parse
* @param parent the parent for the new content
* @param chainedParent whether parent actually corresponds to xmlToParse or has been chained (such as when xmlToParse is a formatting element)
*/
public function parseFlowGroupElementChildren(xmlToParse:XML, parent:FlowGroupElement, exceptionElements:Object = null, chainedParent:Boolean=false):void
{
for each (var child:XML in xmlToParse.children())
{
if (child.nodeKind() == "element")
{
parseObject(child.name().localName, child, parent, exceptionElements);
}
// look for mixed content here
else if (child.nodeKind() == "text")
{
var txt:String = child.toString();
// Strip whitespace-only text appearing as a child of a container-formatted element
var strip:Boolean = false;
if (parent is ContainerFormattedElement)
{
strip = txt.search(anyPrintChar) == -1;
}
if (!strip)
addChild(parent, createImpliedSpan(txt));
}
}
// no implied paragraph should extend across container elements
if (!chainedParent && parent is ContainerFormattedElement)
resetImpliedPara();
}
/**
* Parse XML, convert XML to FlowElements and TextFlow and add to the parent table cell
*
* @param xmlToParse content to parse
* @param parent the parent for the new content
* @param chainedParent whether parent actually corresponds to xmlToParse or has been chained (such as when xmlToParse is a formatting element)
*/
public function parseTableCellElementChildren(xmlToParse:XML, parent:FlowGroupElement, exceptionElements:Object = null, chainedParent:Boolean=false):void
{
var textFlow:TextFlow;
for each (var child:XML in xmlToParse.children())
{
if (child.nodeKind() == "element")
{
if (child.name().localName=="p") {
textFlow = new TextFlow();
parseObject(child.name().localName, child, textFlow, exceptionElements);
}
else if (child.name().localName=="TextFlow") {
TableCellElement(parent).textFlow = createTextFlowFromXML(child);
}
}
// look for mixed content here
else if (child.nodeKind() == "text")
{
var txt:String = child.toString();
// Strip whitespace-only text appearing as a child of a container-formatted element
var strip:Boolean = false;
if (parent is ContainerFormattedElement)
{
strip = txt.search(anyPrintChar) == -1;
}
if (!strip) {
textFlow = new TextFlow();
parseObject(child.name().localName, child, textFlow, exceptionElements);
//addChild(textFlow, createImpliedSpan(txt));
}
}
if (textFlow) {
TableCellElement(parent).textFlow = textFlow;
textFlow = null;
}
}
}
/** create an implied span with specified text */
public function createImpliedSpan(text:String):SpanElement
{
var span:SpanElement = new SpanElement(); // No PMD
span.text = text;
return span;
}
public function createParagraphFlowFromXML(xmlToParse:XML, newFlow:TextFlow = null):TextFlow // No PMD
{
CONFIG::debug { assert(false,"missing override for createParagraphFlowFromXML"); } // client must override
return null;
}
tlf_internal function parseObject(name:String, xmlToParse:XML, parent:FlowGroupElement, exceptionElements:Object=null):void
{
if (!checkNamespace(xmlToParse))
return;
var info:FlowElementInfo = _config.lookup(name);
if (!info)
{
if (exceptionElements == null || exceptionElements[name] === undefined)
handleUnknownElement (name, xmlToParse, parent);
}
else
info.parser(this, xmlToParse, parent);
}
protected function handleUnknownElement(name:String, xmlToParse:XML, parent:FlowGroupElement):void
{
reportError(GlobalSettings.resourceStringFunction("unknownElement", [ name ]));
}
protected function handleUnknownAttribute(elementName:String, propertyName:String):void
{
reportError(GlobalSettings.resourceStringFunction("unknownAttribute", [ propertyName, elementName ]));
}
protected function getElementInfo(xmlToParse:XML):FlowElementInfo
{
return _config.lookup(xmlToParse.name().localName);
}
protected function GetClass(xmlToParse:XML):Class
{
var info:FlowElementInfo = _config.lookup(xmlToParse.name().localName);
return info ? info.flowClass : null;
}
// In the text model, non-FlowParagraphElements (i.e. spans, images, links, TCY) cannot be children of a ContainerElement (TextFlow, Div etc.)
// They can only be children of paragraphs or subparagraph blocks.
// In XML, however, <p> elements can be implied (for example, a <span> may appear as a direct child of <flow>).
// So, while parsing the XML, if we enounter a non-FlowParagraphElement child of a ContainerElement
// 1. an explicitly created paragraph is used as the parent instead
// 2. such explicitly created paragraphs are shared by adjacent flow elements provided there isn't an intervening FlowParagraphElement
private var _impliedPara:ParagraphElement = null;
/** @private */
tlf_internal function createImpliedParagraph():ParagraphElement
{
return createParagraphFromXML(<p/>);
}
/**
* @private
* Helper function for adding a child flow element that honors throwOnError setting and uses the parent override
* NOTE: You MUST NOT call addChild directly unless you are sure
* - There is not possibility of an implied paragraph, and
* - Parent is of type that can contain child
*/
tlf_internal function addChild(parent:FlowGroupElement, child:FlowElement):Boolean
{
if (child is ParagraphFormattedElement)
{
// Reset due to possibly intervening FlowParagrahElement; See note 2. above
resetImpliedPara();
}
else if (parent is ContainerFormattedElement)
{
// See note 1. above
if (!_impliedPara)
{
// Derived classes may have special behavior for <p> tags. Implied paragraphs may need the same behavior.
// So call createParagraphFromXML, don't just instantiate a ParagraphElement
_impliedPara = createImpliedParagraph();
parent.addChild(_impliedPara);
}
parent = _impliedPara;
}
if (throwOnError)
parent.addChild(child);
else
{
try
{
parent.addChild(child);
}
catch (e:*)
{
reportError(e);
return false;
}
}
return true;
}
tlf_internal function resetImpliedPara():void
{
if (_impliedPara)
{
onResetImpliedPara(_impliedPara);
_impliedPara = null;
}
}
protected function onResetImpliedPara(para:ParagraphElement):void
{
}
}
}