|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#nullable disable
using System.Xaml;
using System.Xaml.MS.Impl;
using System.Xaml.Schema;
using System.Xml;
using MS.Internal.Xaml.Context;
namespace MS.Internal.Xaml.Parser
{
class XamlScanner
{
XmlReader _xmlReader;
IXmlLineInfo _xmlLineInfo;
// XamlParserContext vs. XamlScannerStack
// The XamlScannerStack belongs to the Scanner (aka XamlScanner) exclusively.
// It has its own stack because it necessarily will read ahead sometimes.
//
// The XamlParserContext belongs to the Parser (aka XamlParser).
// Except the scanner loads namespaces into the Parser's XamlParserContext,
// and reads from it to resolve type names and namespace prefixes.
//
XamlScannerStack _scannerStack;
XamlParserContext _parserContext;
XamlText _accumulatedText;
List<XamlAttribute> _attributes;
int _nextAttribute;
XamlScannerNode _currentNode;
Queue<XamlScannerNode> _readNodesQueue;
XamlXmlReaderSettings _settings;
XamlAttribute _typeArgumentAttribute;
bool _hasKeyAttribute;
internal XamlScanner(XamlParserContext context, XmlReader xmlReader, XamlXmlReaderSettings settings)
{
_xmlReader = xmlReader;
_xmlLineInfo = settings.ProvideLineInfo ? (xmlReader as IXmlLineInfo) : null; //consider removing the "settings" check
_parserContext = context;
_scannerStack = new XamlScannerStack();
_readNodesQueue = new Queue<XamlScannerNode>();
_settings = settings;
if (settings.XmlSpacePreserve)
{
_scannerStack.CurrentXmlSpacePreserve = true;
}
}
public void Read()
{
LoadQueue();
_currentNode = _readNodesQueue.Dequeue();
}
public ScannerNodeType PeekNodeType
{
get
{
LoadQueue();
return _readNodesQueue.Peek().NodeType;
}
}
public XamlType PeekType
{
get
{
LoadQueue();
return _readNodesQueue.Peek().Type;
}
}
public ScannerNodeType NodeType
{
get { return _currentNode.NodeType; }
}
public XamlType Type
{
get { return _currentNode.Type; }
}
public XamlMember PropertyAttribute
{
get { return _currentNode.PropertyAttribute; }
}
public XamlText PropertyAttributeText
{
get { return _currentNode.PropertyAttributeText; }
}
public bool IsCtorForcingMember
{
get { return _currentNode.IsCtorForcingMember; }
}
public XamlMember PropertyElement
{
get { return _currentNode.PropertyElement; }
}
public XamlText TextContent
{
get { return _currentNode.TextContent; }
}
public bool IsXDataText
{
get { return _currentNode.IsXDataText; }
}
public bool HasKeyAttribute
{
get
{
return _hasKeyAttribute;
}
}
public string Prefix
{
get { return _currentNode.Prefix; }
}
public string Namespace
{
get { return _currentNode.TypeNamespace; }
}
public int LineNumber
{
get { return _currentNode.LineNumber; }
}
public int LinePosition
{
get { return _currentNode.LinePosition; }
}
// ===================================================================
private void LoadQueue()
{
if (_readNodesQueue.Count == 0)
{
DoXmlRead();
}
}
private void DoXmlRead()
{
while (_readNodesQueue.Count == 0)
{
if (_xmlReader.Read())
{
ProcessCurrentXmlNode();
}
else
{
// false from XML Read() is the same as reading .None.
ReadNone();
}
}
}
private void ProcessCurrentXmlNode()
{
XmlNodeType xmlNodeType = _xmlReader.NodeType;
switch (xmlNodeType)
{
case XmlNodeType.Element:
ReadElement();
break;
case XmlNodeType.EndElement:
ReadEndElement();
break;
case XmlNodeType.Text:
case XmlNodeType.CDATA:
ReadText();
break;
case XmlNodeType.SignificantWhitespace:
case XmlNodeType.Whitespace:
ReadWhitespace();
break;
case XmlNodeType.None:
ReadNone();
break;
default:
break;
}
}
// ============= Private ==================================
private XamlText AccumulatedText
{
get
{
if (_accumulatedText is null)
{
_accumulatedText = new XamlText(_scannerStack.CurrentXmlSpacePreserve);
}
return _accumulatedText;
}
}
private void ClearAccumulatedText()
{
_accumulatedText = null;
}
private bool HaveAccumulatedText
{
get { return _accumulatedText is not null && !_accumulatedText.IsEmpty; }
}
// ============= Element Processing ==================================
private void ReadElement()
{
// Accumulated text is enqueued a piece of content before this element
EnqueueAnyText();
_hasKeyAttribute = false;
// Empty Elements are by definition leaf elements and they
// don't nest. So we don't need to stack this state.
bool isEmptyTag = _xmlReader.IsEmptyElement;
string prefix = _xmlReader.Prefix;
string strippedName = _xmlReader.LocalName;
if (XamlName.ContainsDot(strippedName))
{
Debug.Assert(_xmlReader.NodeType == XmlNodeType.Element);
XamlPropertyName name = XamlPropertyName.Parse(_xmlReader.Name, _xmlReader.NamespaceURI);
if (_scannerStack.CurrentType is null)
{
throw LineInfo(new XamlParseException(SR.Format(SR.ParentlessPropertyElement, _xmlReader.Name)));
}
ReadPropertyElement(name, _scannerStack.CurrentType, _scannerStack.CurrentTypeNamespace, isEmptyTag);
}
else
{
XamlName name = new XamlQualifiedName(prefix, strippedName);
ReadObjectElement(name, isEmptyTag);
}
}
// ReadObjectElement: reads the entire start tag. This may result in
// more than one Scanner Nodes. The results are enqueued onto a list
// rather than returned directly, then the caller can de-queue the nodes
// one at a time.
private void ReadObjectElement(XamlName name, bool isEmptyTag)
{
_typeArgumentAttribute = null;
XamlScannerNode node = new XamlScannerNode(_xmlLineInfo);
// Scan for xmlns(s) before attempting to resolve the type.
// So while we are there, collect up all the attributes.
// Enqueue the xmlns attributes first.
// PostProcess and Enqueue the other attributes after.
PreprocessAttributes();
node.Prefix = name.Prefix;
node.IsEmptyTag = isEmptyTag;
// It is possible for an application to provide XML nodes via XmlNodeReader
// where the URI is defined but there was no xmlns attribute for use to resolve against.
// See app Paperboy
Debug.Assert(_xmlReader.NodeType == XmlNodeType.Element);
string xamlNs = _xmlReader.NamespaceURI;
if (xamlNs is null)
{
ReadObjectElement_NoNamespace(name, node);
}
else // if (xamlNs != null)
{
node.TypeNamespace = xamlNs;
// First check if the XML element is a
// Directive Property <x:Key>
//
XamlSchemaContext schemaContext = _parserContext.SchemaContext;
XamlMember dirProperty = schemaContext.GetXamlDirective(xamlNs, name.Name);
if (dirProperty is not null)
{
ReadObjectElement_DirectiveProperty(dirProperty, node);
}
else // normal Element Case.
{
bool sawXData = ReadObjectElement_Object(xamlNs, name.Name, node);
if (sawXData)
{
return;
}
}
}
_readNodesQueue.Enqueue(node);
// Now add the processed attributes from the rest of the start tag.
while (HaveUnprocessedAttributes)
{
EnqueueAnotherAttribute(isEmptyTag);
}
}
private void ReadObjectElement_NoNamespace(XamlName name, XamlScannerNode node)
{
XamlType errType = CreateErrorXamlType(name, string.Empty);
node.Type = errType;
PostprocessAttributes(node);
if (!node.IsEmptyTag)
{
node.NodeType = ScannerNodeType.ELEMENT;
_scannerStack.Push(node.Type, node.TypeNamespace);
}
else
{
node.NodeType = ScannerNodeType.EMPTYELEMENT;
}
}
private void ReadObjectElement_DirectiveProperty(XamlMember dirProperty, XamlScannerNode node)
{
node.PropertyElement = dirProperty;
// node.Type is not set so any found attributes are and error.
// We don't want to skip them silently.
PostprocessAttributes(node);
if (_scannerStack.Depth > 0)
{
// A property Element tag will be the end of content.
_scannerStack.CurrentlyInContent = false;
}
if (!node.IsEmptyTag)
{
_scannerStack.CurrentProperty = node.PropertyElement;
}
node.NodeType = ScannerNodeType.PROPERTYELEMENT;
node.IsCtorForcingMember = false;
}
private bool ReadObjectElement_Object(string xmlns, string name, XamlScannerNode node)
{
if (IsXDataElement(xmlns, name))
{
// If XData don't Enqueue the <x:XData> node.
// just queue the InnerXml as TEXT (w/ IsTextXML == true).
// This will advance the "current" xml node to the </x:XData>
// which will be skipped when we return and the main loop
// Read()s to the "next" XmlNode.
ReadInnerXDataSection();
return true;
}
IList<XamlTypeName> typeArgs = null;
if (_typeArgumentAttribute is not null)
{
string error;
typeArgs = XamlTypeName.ParseListInternal(_typeArgumentAttribute.Value, _parserContext.FindNamespaceByPrefix, out error);
if (typeArgs is null)
{
throw new XamlParseException(_typeArgumentAttribute.LineNumber, _typeArgumentAttribute.LinePosition, error);
}
}
XamlTypeName typeName = new XamlTypeName(xmlns, name, typeArgs);
node.Type = _parserContext.GetXamlType(typeName, true);
// Finish initializing the attributes in the context of the
// current Element.
PostprocessAttributes(node);
if (_scannerStack.Depth > 0)
{
// Sub-elements (and Text) are the definition of Content
_scannerStack.CurrentlyInContent = true;
}
if (!node.IsEmptyTag)
{
node.NodeType = ScannerNodeType.ELEMENT;
_scannerStack.Push(node.Type, node.TypeNamespace);
}
else
{
node.NodeType = ScannerNodeType.EMPTYELEMENT;
}
return false;
}
private void ReadPropertyElement(XamlPropertyName name, XamlType tagType, string tagNamespace, bool isEmptyTag)
{
// <Button> <== currentElement
// <FrameworkElement.Width> <== FrameworkElement is ownerType
XamlScannerNode node = new XamlScannerNode(_xmlLineInfo);
// Attributes aren't allowed on property elements.
// but if they are there we need to scan them so the
// XamlParser can error, or whatever.
// (don't want to skip them w/o error)
PreprocessAttributes();
// It is possible for an application to provide XML nodes via XmlNodeReader
// where the URI is defined but there was no xmlns attribute for use to resolve against.
// See app Paperboy
Debug.Assert(_xmlReader.NodeType == XmlNodeType.Element);
string ownerNamespace = _xmlReader.NamespaceURI;
XamlMember property = null;
bool tagIsRoot = _scannerStack.Depth == 1; // PEs are processed after frame is pushed
property = _parserContext.GetDottedProperty(tagType, tagNamespace, name, tagIsRoot);
node.Prefix = name.Prefix;
node.TypeNamespace = ownerNamespace;
node.IsEmptyTag = isEmptyTag;
// node.Type is not set (this is a property)
// so this processing does less.
PostprocessAttributes(node);
if (_scannerStack.Depth > 0)
{
// A property Element tag will be the end of content.
// This also allows to to start content again.
// That is an error, but at least the parser/scanner can
// understand what is going on.
_scannerStack.CurrentlyInContent = false;
}
node.PropertyElement = property;
node.IsCtorForcingMember = !property.IsAttachable;
if (!node.IsEmptyTag)
{
_scannerStack.CurrentProperty = node.PropertyElement;
node.NodeType = ScannerNodeType.PROPERTYELEMENT;
}
else
{
node.NodeType = ScannerNodeType.EMPTYPROPERTYELEMENT;
}
_readNodesQueue.Enqueue(node);
while (HaveUnprocessedAttributes)
{
EnqueueAnotherAttribute(isEmptyTag);
}
}
private void ReadEndElement()
{
// Accumulated text is enqued at start of tags (element and property)
// and at end tags.
EnqueueAnyText();
// if we are ending a property element tag clear the current property
// if we are ending an element then pop off the current frame.
if (_scannerStack.CurrentProperty is not null)
{
_scannerStack.CurrentProperty = null;
// List of Content is considered separately for each property.
_scannerStack.CurrentlyInContent = false;
}
else
{
_scannerStack.Pop();
}
XamlScannerNode node = new XamlScannerNode(_xmlLineInfo);
node.NodeType = ScannerNodeType.ENDTAG;
_readNodesQueue.Enqueue(node);
}
private void ReadText()
{
// Trim the leading whitespace from the text if it is the first bit on content.
bool isFirstTextInContent = !_scannerStack.CurrentlyInContent;
AccumulatedText.Paste(_xmlReader.Value, isFirstTextInContent);
_scannerStack.CurrentlyInContent = true;
}
private void ReadWhitespace()
{
bool isFirstTextInContent = !_scannerStack.CurrentlyInContent;
AccumulatedText.Paste(_xmlReader.Value, isFirstTextInContent);
// Whitespace, by itself, does not change the "InContent" state.
}
private void ReadNone()
{
XamlScannerNode node = new XamlScannerNode(_xmlLineInfo);
node.NodeType = ScannerNodeType.NONE;
_readNodesQueue.Enqueue(node);
}
private void ReadInnerXDataSection()
{
XamlScannerNode node = new XamlScannerNode(_xmlLineInfo);
_xmlReader.MoveToContent(); // skip whitespaces
string xmlData = _xmlReader.ReadInnerXml();
xmlData = xmlData.Trim();
node.NodeType = ScannerNodeType.TEXT;
node.IsXDataText = true;
XamlText xmlText = new XamlText(true);
xmlText.Paste(xmlData, false);
node.TextContent = xmlText;
_readNodesQueue.Enqueue(node);
// Read InnerXml will advance over the End xData tag and
// we need to process the current XML state w/o going back for a Read().
ProcessCurrentXmlNode();
}
private XamlType CreateErrorXamlType(XamlName name, string xmlns)
{
return new XamlType(xmlns, name.Name, null, _parserContext.SchemaContext);
}
// ======== Attribute Processing =============================
private void PreprocessAttributes()
{
// Collect up all the attributes.
bool b = _xmlReader.MoveToFirstAttribute();
if (!b)
{
return;
}
List<XamlAttribute> list = new List<XamlAttribute>();
do
{
string xmlName = _xmlReader.Name;
string val = _xmlReader.Value;
XamlPropertyName propName = XamlPropertyName.Parse(xmlName);
if (propName is null)
{
throw new XamlParseException(SR.Format(SR.InvalidXamlMemberName, xmlName));
}
XamlAttribute attr = new XamlAttribute(propName, val, _xmlLineInfo);
if (attr.Kind == ScannerAttributeKind.Namespace)
{
EnqueuePrefixDefinition(attr);
}
else
{
list.Add(attr);
}
b = _xmlReader.MoveToNextAttribute();
} while (b);
PreprocessForTypeArguments(list);
if (list.Count > 0)
{
_attributes = list;
}
// Restore the XML reader’s position to the Element after reading the
// attributes so that the rest of the code can always assume it is on an Element
_xmlReader.MoveToElement();
}
private void PreprocessForTypeArguments(List<XamlAttribute> attrList)
{
int typeArgsIdx = -1;
for (int i = 0; i < attrList.Count; i++)
{
XamlAttribute attr = attrList[i];
// Find x:TypeArguments if it was present.
if (KS.Eq(attr.Name.Name, XamlLanguage.TypeArguments.Name))
{
string attrNamespace = _parserContext.FindNamespaceByPrefix(attr.Name.Prefix);
XamlMember directiveProperty = _parserContext.ResolveDirectiveProperty(attrNamespace, attr.Name.Name);
if (directiveProperty is not null)
{
typeArgsIdx = i;
_typeArgumentAttribute = attr;
break;
}
}
}
if (typeArgsIdx >= 0)
{
attrList.RemoveAt(typeArgsIdx);
}
}
private void PostprocessAttributes(XamlScannerNode node)
{
if (_attributes is null)
{
return;
}
_nextAttribute = 0;
// Attributes on Properties are errors
// and don't need this detailed processing.
if (node.Type is null)
{
if (_settings.IgnoreUidsOnPropertyElements)
{
StripUidProperty();
}
return;
}
bool tagIsRoot = _scannerStack.Depth == 0; // Attributes are processed before frame is pushed
foreach (XamlAttribute attr in _attributes)
{
attr.Initialize(_parserContext, node.Type, node.TypeNamespace, tagIsRoot);
}
// Sort the Attributes into the order the XAML parser likes.
List<XamlAttribute> ctorDirectivesList = null;
List<XamlAttribute> otherDirectivesList = null;
List<XamlAttribute> otherPropertiesList = null;
XamlAttribute nameAttribute = null;
// The Name attribute
foreach (XamlAttribute attr in _attributes)
{
switch(attr.Kind)
{
case ScannerAttributeKind.Name:
nameAttribute = attr;
break;
case ScannerAttributeKind.CtorDirective:
if (ctorDirectivesList is null)
{
ctorDirectivesList = new List<XamlAttribute>();
}
ctorDirectivesList.Add(attr);
break;
case ScannerAttributeKind.Directive:
case ScannerAttributeKind.XmlSpace:
if (attr.Property == XamlLanguage.Key)
{
_hasKeyAttribute = true;
}
if (otherDirectivesList is null)
{
otherDirectivesList = new List<XamlAttribute>();
}
otherDirectivesList.Add(attr);
break;
default:
if (otherPropertiesList is null)
{
otherPropertiesList = new List<XamlAttribute>();
}
otherPropertiesList.Add(attr);
break;
}
}
_attributes = new List<XamlAttribute>();
// First the Construction Directives
if (ctorDirectivesList is not null)
{
_attributes.AddRange(ctorDirectivesList);
}
if (otherDirectivesList is not null)
{
_attributes.AddRange(otherDirectivesList);
}
// Next the aliased Name property before any other "real" properties.
// (this is a WPF template requirement)
if (nameAttribute is not null)
{
_attributes.Add(nameAttribute);
}
// Then everything else
if (otherPropertiesList is not null)
{
_attributes.AddRange(otherPropertiesList);
}
}
private void StripUidProperty()
{
for (int i = _attributes.Count - 1; i >= 0; i--)
{
if (KS.Eq(_attributes[i].Name.ScopedName, XamlLanguage.Uid.Name))
{
_attributes.RemoveAt(i);
}
}
if (_attributes.Count == 0)
{
_attributes = null;
}
}
private bool HaveUnprocessedAttributes
{
get { return _attributes is not null; }
}
private void EnqueueAnotherAttribute(bool isEmptyTag)
{
XamlAttribute attr = _attributes[_nextAttribute++];
XamlScannerNode node = new XamlScannerNode(attr);
switch (attr.Kind)
{
case ScannerAttributeKind.Directive:
case ScannerAttributeKind.Name:
case ScannerAttributeKind.CtorDirective:
node.NodeType = ScannerNodeType.DIRECTIVE;
break;
case ScannerAttributeKind.XmlSpace:
// Empty tags don't have a stack frame to write on.
// Empty XML tags don't have content to process spaces.
if (!isEmptyTag)
{
if (KS.Eq(attr.Value, KnownStrings.Preserve))
_scannerStack.CurrentXmlSpacePreserve = true;
else
_scannerStack.CurrentXmlSpacePreserve = false;
}
node.NodeType = ScannerNodeType.DIRECTIVE;
break;
case ScannerAttributeKind.Event:
case ScannerAttributeKind.Property:
node.IsCtorForcingMember = true;
node.NodeType = ScannerNodeType.ATTRIBUTE;
break;
case ScannerAttributeKind.Unknown:
XamlMember prop = attr.Property;
Debug.Assert(prop.IsUnknown);
// force use of Ctor for unknown simple properties only
node.IsCtorForcingMember = !prop.IsAttachable && !prop.IsDirective;
node.NodeType = ScannerNodeType.ATTRIBUTE;
break;
case ScannerAttributeKind.AttachableProperty:
node.NodeType = ScannerNodeType.ATTRIBUTE;
break;
default:
throw new XamlInternalException(SR.AttributeUnhandledKind);
}
// (GetFixedDocumentSequence raises Exception "UnicodeString property does not
// contain enough characters to correspond to the contents of Indices property.")
//
// XamlText.Paste normally converts CRLF to LF, even in attribute values.
// When the property is Glyphs.UnicodeString, disable this;
// the length of the string must correspond to the number of entries in
// the corresponding Glyphs.Indices property.
XamlMember attrProperty = attr.Property;
bool convertCRLFtoLF =
!(attrProperty is not null &&
attrProperty.Name == "UnicodeString" &&
attrProperty.DeclaringType.Name == "Glyphs");
node.PropertyAttribute = attrProperty;
XamlText xamlText = new XamlText(true); // Don't collapse spaces in attributes
xamlText.Paste(attr.Value, false, convertCRLFtoLF);
node.PropertyAttributeText = xamlText;
node.Prefix = attr.Name.Prefix;
_readNodesQueue.Enqueue(node);
if (_nextAttribute >= _attributes.Count)
{
_attributes = null;
_nextAttribute = -1;
}
}
private void EnqueueAnyText()
{
if (HaveAccumulatedText)
{
// some consideration of _scannerStack.CurrentXmlSpacePreserve
// it will cause Whitespace here but we only enqueue it if
// we are In a Whitespace significant collection.
EnqueueTextNode();
}
ClearAccumulatedText();
}
private void EnqueueTextNode()
{
Debug.Assert(_accumulatedText is not null, "Creating unnecessary XamlText objects");
// Don't send the text if it is Whitespace outside the root tag.
if (!(_scannerStack.Depth == 0 && AccumulatedText.IsWhiteSpaceOnly))
{
XamlScannerNode node = new XamlScannerNode(_xmlLineInfo);
node.NodeType = ScannerNodeType.TEXT;
node.TextContent = AccumulatedText;
_readNodesQueue.Enqueue(node);
}
}
private void EnqueuePrefixDefinition(XamlAttribute attr)
{
string prefix = attr.XmlNsPrefixDefined;
string xamlNamespace = attr.XmlNsUriDefined;
_parserContext.AddNamespacePrefix(prefix, xamlNamespace);
XamlScannerNode node = new XamlScannerNode(attr);
node.NodeType = ScannerNodeType.PREFIXDEFINITION;
node.Prefix = prefix;
node.TypeNamespace = xamlNamespace;
_readNodesQueue.Enqueue(node);
}
private bool IsXDataElement(string xmlns, string name)
{
return
XamlLanguage.XamlNamespaces.Contains(xmlns) &&
KS.Eq(XamlLanguage.XData.Name, name);
}
XamlException LineInfo(XamlException e)
{
if (_xmlLineInfo is not null)
{
e.SetLineInfo(_xmlLineInfo.LineNumber, _xmlLineInfo.LinePosition);
}
return e;
}
}
}
|