//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // // [....] //------------------------------------------------------------------------------ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Globalization; using System.Xml; using System.Xml.XPath; namespace System.Xml.Xsl.Runtime { using Res = System.Xml.Utils.Res; internal enum XmlState { WithinSequence = 0, // Adding items to a top-level sequence EnumAttrs, // Adding attributes to an element WithinContent, // Adding content to an element WithinAttr, // Adding text to an attribute WithinNmsp, // Adding text to an namespace WithinComment, // Adding text to a comment WithinPI, // Adding text to a processing instruction }; /// /// At run-time, a number of checks may need to be made in order to generate the correct sequence of calls /// to XmlRawWriter: /// 1. Well-formedness: Illegal state transitions, StartContent detection, no-content element detection /// 2. Cached attributes: In XSLT, attributes override previously constructed attributes with the same name, /// meaning that attribute names and values cannot be prematurely sent to XmlRawWriter. /// 3. Cached namespaces: All namespaces are tracked in order to ensure adequate namespaces and to ensure /// minimal (or something close) namespaces. /// [EditorBrowsable(EditorBrowsableState.Never)] public sealed class XmlQueryOutput : XmlWriter { // Never set these fields directly--instead use corresponding properties private XmlRawWriter xwrt; // Output to XmlRawWriter--get and set this using the Writer property // It is OK to set these properties directly private XmlQueryRuntime runtime; // The XmlQueryRuntime instance that keeps global state private XmlAttributeCache attrCache; // Cache used to detect duplicate attributes private int depth; // Depth of the currently constructing tree private XmlState xstate; // Current XML state private XmlSequenceWriter seqwrt; // Current XmlSequenceWriter private XmlNamespaceManager nsmgr; // Output namespace manager private int cntNmsp; // Number of pending namespaces private Dictionary conflictPrefixes; // Remembers prefixes that were auto-generated previously in case they can be reused private int prefixIndex; // Counter used to auto-generate non-conflicting attribute prefixes private string piTarget/*nmspPrefix*/; // Cache pi target or namespace prefix private StringConcat nodeText; // Cache pi, comment, or namespace text private Stack stkNames; // Keep stack of name parts computed during StartElement private XPathNodeType rootType; // NodeType of the root of the tree private Dictionary usedPrefixes = new Dictionary(); //The prefies that used in the current scope /// /// This constructor is internal so that external users cannot construct it (and therefore we do not have to test it separately). /// Initialize output state to accept top-level sequences. /// internal XmlQueryOutput(XmlQueryRuntime runtime, XmlSequenceWriter seqwrt) { this.runtime = runtime; this.seqwrt = seqwrt; this.xstate = XmlState.WithinSequence; } /// /// This constructor is internal so that external users cannot construct it (and therefore we do not have to test it separately). /// Initialize output state to accept Rtf content (top-level sequences are therefore prohibited). /// internal XmlQueryOutput(XmlQueryRuntime runtime, XmlEventCache xwrt) { this.runtime = runtime; this.xwrt = xwrt; this.xstate = XmlState.WithinContent; this.depth = 1; this.rootType = XPathNodeType.Root; } /// /// Sequence writer to which output is directed by this class. /// internal XmlSequenceWriter SequenceWriter { get { return this.seqwrt; } } /// /// Raw writer to which output is directed by this class. /// internal XmlRawWriter Writer { get { return this.xwrt; } set { // If new writer might remove itself from pipeline, have it callback on this method when it's ready to go IRemovableWriter removable = value as IRemovableWriter; if (removable != null) removable.OnRemoveWriterEvent = SetWrappedWriter; this.xwrt = value; } } /// /// This method will be called if "xwrt" is a writer which no longer needs to be part of the pipeline and /// wishes to replace itself with a different writer. For example, the auto-detect writer replaces itself /// with the Html or Xml writer once it has determined which output mode to use. /// private void SetWrappedWriter(XmlRawWriter writer) { // Reuse XmlAttributeCache so that it doesn't have to be recreated every time if (Writer is XmlAttributeCache) this.attrCache = (XmlAttributeCache) Writer; Writer = writer; } //----------------------------------------------- // XmlWriter methods //----------------------------------------------- /// /// Should never be called. /// public override void WriteStartDocument() { throw new NotSupportedException(); } /// /// Should never be called. /// public override void WriteStartDocument(bool standalone) { throw new NotSupportedException(); } /// /// Should never be called. /// public override void WriteEndDocument() { throw new NotSupportedException(); } /// /// Should never be called. /// public override void WriteDocType(string name, string pubid, string sysid, string subset) { throw new NotSupportedException(); } /// /// Before calling XmlRawWriter.WriteStartElement(), perform various checks to ensure well-formedness. /// public override void WriteStartElement(string prefix, string localName, string ns) { Debug.Assert(prefix != null && localName != null && localName.Length != 0 && ns != null, "Invalid argument"); Debug.Assert(ValidateNames.ValidateName(prefix, localName, ns, XPathNodeType.Element, ValidateNames.Flags.All), "Name validation failed"); // Xml state transitions ConstructWithinContent(XPathNodeType.Element); // Call XmlRawWriter.WriteStartElement WriteStartElementUnchecked(prefix, localName, ns); // Ensure that element's namespace declaration is declared WriteNamespaceDeclarationUnchecked(prefix, ns); // Cache attributes in order to detect duplicates if (this.attrCache == null) this.attrCache = new XmlAttributeCache(); this.attrCache.Init(Writer); Writer = this.attrCache; this.attrCache = null; // Push element names onto a stack PushElementNames(prefix, localName, ns); } /// /// Before calling XmlRawWriter.WriteEndElement(), perform various checks to ensure well-formedness. /// public override void WriteEndElement() { string prefix, localName, ns; // Determine whether element had no content if (this.xstate == XmlState.EnumAttrs) { // No content, so call StartElementContent now StartElementContentUnchecked(); } // Call XmlRawWriter.WriteEndElement PopElementNames(out prefix, out localName, out ns); WriteEndElementUnchecked(prefix, localName, ns); // Xml state transitions if (this.depth == 0) EndTree(); } /// /// Same as calling WriteEndElement(). /// public override void WriteFullEndElement() { WriteEndElement(); } /// /// Before calling XmlRawWriter.WriteStartAttribute(), perform various checks to ensure well-formedness. /// public override void WriteStartAttribute(string prefix, string localName, string ns) { Debug.Assert(prefix != null && localName != null && ns != null, "Invalid argument"); if (prefix.Length == 5 && prefix == "xmlns") { // Handle namespace attributes that are not sent directly to WriteNamespaceDeclaration WriteStartNamespace(localName); } else { // All other attributes Debug.Assert(ValidateNames.ValidateName(prefix, localName, ns, XPathNodeType.Attribute, ValidateNames.Flags.All)); // Xml state transitions ConstructInEnumAttrs(XPathNodeType.Attribute); // Check for prefix conflicts and possibly declare prefix if (ns.Length != 0 && this.depth != 0) prefix = CheckAttributePrefix(prefix, ns); // Output the attribute WriteStartAttributeUnchecked(prefix, localName, ns); } } /// /// Before calling XmlRawWriter.WriteEndAttribute(), perform various checks to ensure well-formedness. /// public override void WriteEndAttribute() { if (this.xstate == XmlState.WithinNmsp) { WriteEndNamespace(); } else { WriteEndAttributeUnchecked(); if (this.depth == 0) EndTree(); } } /// /// Before writing a comment, perform various checks to ensure well-formedness. /// public override void WriteComment(string text) { WriteStartComment(); WriteCommentString(text); WriteEndComment(); } /// /// Before writing a processing instruction, perform various checks to ensure well-formedness. /// public override void WriteProcessingInstruction(string target, string text) { WriteStartProcessingInstruction(target); WriteProcessingInstructionString(text); WriteEndProcessingInstruction(); } /// /// Should never be called. /// public override void WriteEntityRef(string name) { throw new NotSupportedException(); } /// /// Should never be called. /// public override void WriteCharEntity(char ch) { throw new NotSupportedException(); } /// /// Should never be called. /// public override void WriteSurrogateCharEntity(char lowChar, char highChar) { throw new NotSupportedException(); } /// /// Treat whitespace as regular text. /// public override void WriteWhitespace(string ws) { throw new NotSupportedException(); } /// /// Before writing text, perform various checks to ensure well-formedness. /// public override void WriteString(string text) { WriteString(text, false); } /// /// Before writing text, perform various checks to ensure well-formedness. /// public override void WriteChars(char[] buffer, int index, int count) { throw new NotSupportedException(); } /// /// Write text, but do not escape special characters. /// public override void WriteRaw(char[] buffer, int index, int count) { throw new NotSupportedException(); } /// /// Write text, but do not escape special characters. /// public override void WriteRaw(string data) { WriteString(data, true); } /// /// Write CData text as regular text. /// public override void WriteCData(string text) { WriteString(text, false); } /// /// Should never be called. /// public override void WriteBase64(byte[] buffer, int index, int count) { throw new NotSupportedException(); } /// /// Should never be called. /// public override WriteState WriteState { get { throw new NotSupportedException(); } } /// /// No-op. /// public override void Close() { } /// /// No-op. /// public override void Flush() { } /// /// Should never be called. /// public override string LookupPrefix(string ns) { throw new NotSupportedException(); } /// /// Should never be called. /// public override XmlSpace XmlSpace { get { throw new NotSupportedException(); } } /// /// Should never be called. /// public override string XmlLang { get { throw new NotSupportedException(); } } //----------------------------------------------- // XmlQueryOutput methods (XmlSequenceWriter) //----------------------------------------------- /// /// Call XmlSequenceWriter.StartTree() in order to start construction of a new tree. /// public void StartTree(XPathNodeType rootType) { Debug.Assert(this.xstate == XmlState.WithinSequence, "StartTree cannot be called in the " + this.xstate + " state."); Writer = this.seqwrt.StartTree(rootType, this.nsmgr, this.runtime.NameTable); this.rootType = rootType; this.xstate = (rootType == XPathNodeType.Attribute || rootType == XPathNodeType.Namespace) ? XmlState.EnumAttrs : XmlState.WithinContent; } /// /// Call XmlSequenceWriter.EndTree(). /// public void EndTree() { Debug.Assert(this.xstate == XmlState.EnumAttrs || this.xstate == XmlState.WithinContent, "EndTree cannot be called in the " + this.xstate + " state."); this.seqwrt.EndTree(); this.xstate = XmlState.WithinSequence; Writer = null; } //----------------------------------------------- // XmlQueryOutput methods (XmlRawWriter) //----------------------------------------------- /// /// Call XmlRawWriter.WriteStartElement() with prefix, local-name, ns, and schema type. /// public void WriteStartElementUnchecked(string prefix, string localName, string ns) { Debug.Assert(this.xstate == XmlState.WithinContent, "WriteStartElement cannot be called in the " + this.xstate + " state."); if (this.nsmgr != null) this.nsmgr.PushScope(); Writer.WriteStartElement(prefix, localName, ns); //reset when enter element usedPrefixes.Clear(); usedPrefixes[prefix] = ns; this.xstate = XmlState.EnumAttrs; this.depth++; } /// /// Call XmlRawWriter.WriteStartElement() with empty prefix, ns, and null schema type. /// public void WriteStartElementUnchecked(string localName) { WriteStartElementUnchecked(string.Empty, localName, string.Empty); } /// /// Call XmlRawWriter.StartElementContent(). /// public void StartElementContentUnchecked() { Debug.Assert(this.xstate == XmlState.EnumAttrs, "StartElementContent cannot be called in the " + this.xstate + " state."); // Output any cached namespaces if (this.cntNmsp != 0) WriteCachedNamespaces(); Writer.StartElementContent(); this.xstate = XmlState.WithinContent; } /// /// Call XmlRawWriter.WriteEndElement() with prefix, local-name, and ns. /// public void WriteEndElementUnchecked(string prefix, string localName, string ns) { Debug.Assert(this.xstate == XmlState.EnumAttrs || this.xstate == XmlState.WithinContent, "WriteEndElement cannot be called in the " + this.xstate + " state."); Writer.WriteEndElement(prefix, localName, ns); this.xstate = XmlState.WithinContent; this.depth--; if (this.nsmgr != null) this.nsmgr.PopScope(); } /// /// Call XmlRawWriter.WriteEndElement() with empty prefix, ns. /// public void WriteEndElementUnchecked(string localName) { WriteEndElementUnchecked(string.Empty, localName, string.Empty); } /// /// XmlRawWriter.WriteStartAttribute() with prefix, local-name, ns, and schema type. /// public void WriteStartAttributeUnchecked(string prefix, string localName, string ns) { Debug.Assert(this.xstate == XmlState.EnumAttrs, "WriteStartAttribute cannot be called in the " + this.xstate + " state."); Writer.WriteStartAttribute(prefix, localName, ns); this.xstate = XmlState.WithinAttr; this.depth++; } /// /// XmlRawWriter.WriteStartAttribute() with empty prefix, ns, and null schema type. /// public void WriteStartAttributeUnchecked(string localName) { WriteStartAttributeUnchecked(string.Empty, localName, string.Empty); } /// /// XmlRawWriter.WriteEndAttribute(). /// public void WriteEndAttributeUnchecked() { Debug.Assert(this.xstate == XmlState.WithinAttr, "WriteEndAttribute cannot be called in the " + this.xstate + " state."); Writer.WriteEndAttribute(); this.xstate = XmlState.EnumAttrs; this.depth--; } /// /// Add a new namespace declaration -- xmlns:prefix="ns" -- to the set of in-scope declarations. /// NOTE: This method should only be called if caller can guarantee that the current state is EnumAttrs /// and that there will be no namespace conflicts in the current scope (e.g. trying to map the /// same prefix to different namespaces within the same element start tag). If no such /// guarantees exist, then WriteNamespaceDeclaration() should be called instead. /// public void WriteNamespaceDeclarationUnchecked(string prefix, string ns) { Debug.Assert(prefix != null && ns != null); Debug.Assert(this.xstate == XmlState.EnumAttrs, "WriteNamespaceDeclaration cannot be called in the " + this.xstate + " state."); // xmlns:foo="" is illegal Debug.Assert(prefix.Length == 0 || ns.Length != 0); if (this.depth == 0) { // At top-level, so write namespace declaration directly to output Writer.WriteNamespaceDeclaration(prefix, ns); return; } if (this.nsmgr == null) { // If namespace manager has no namespaces, then xmlns="" is in scope by default if (ns.Length == 0 && prefix.Length == 0) return; this.nsmgr = new XmlNamespaceManager(this.runtime.NameTable); this.nsmgr.PushScope(); } if (this.nsmgr.LookupNamespace(prefix) != ns) AddNamespace(prefix, ns); usedPrefixes[prefix] = ns; } /// /// Write a text block to the XmlRawWriter. /// public void WriteStringUnchecked(string text) { Debug.Assert(this.xstate != XmlState.WithinSequence && this.xstate != XmlState.EnumAttrs, "WriteTextBlock cannot be called in the " + this.xstate + " state."); Writer.WriteString(text); } /// /// Write a text block without escaping special characters. /// public void WriteRawUnchecked(string text) { Debug.Assert(this.xstate != XmlState.WithinSequence && this.xstate != XmlState.EnumAttrs, "WriteTextBlockNoEntities cannot be called in the " + this.xstate + " state."); Writer.WriteRaw(text); } //----------------------------------------------- // XmlQueryOutput methods //----------------------------------------------- /// /// Before calling XmlSequenceWriter.StartTree(), perform checks to ensure well-formedness. /// public void WriteStartRoot() { Debug.Assert(this.depth == 0, "Root node can only be constructed at top-level."); if (this.xstate != XmlState.WithinSequence) ThrowInvalidStateError(XPathNodeType.Root); StartTree(XPathNodeType.Root); this.depth++; } /// /// Call XmlSequenceWriter.EndTree() and reset state. /// public void WriteEndRoot() { Debug.Assert(this.depth == 1, "Root node can only be constructed at top-level."); this.depth--; EndTree(); } /// /// WriteStartElement() with empty prefix, ns. /// public void WriteStartElementLocalName(string localName) { WriteStartElement(string.Empty, localName, string.Empty); } /// /// WriteStartAttribute() with empty prefix, ns, and null schema type. /// public void WriteStartAttributeLocalName(string localName) { WriteStartAttribute(string.Empty, localName, string.Empty); } /// /// Write an element with a name that is computed from a "prefix:localName" tag name and a set of prefix mappings. /// public void WriteStartElementComputed(string tagName, int prefixMappingsIndex) { WriteStartComputed(XPathNodeType.Element, tagName, prefixMappingsIndex); } /// /// Write an element with a name that is computed from a "prefix:localName" tag name and a namespace URI. /// public void WriteStartElementComputed(string tagName, string ns) { WriteStartComputed(XPathNodeType.Element, tagName, ns); } /// /// Write an element with a name that is copied from the navigator. /// public void WriteStartElementComputed(XPathNavigator navigator) { WriteStartComputed(XPathNodeType.Element, navigator); } /// /// Write an element with a name that is derived from the XmlQualifiedName. /// public void WriteStartElementComputed(XmlQualifiedName name) { WriteStartComputed(XPathNodeType.Element, name); } /// /// Write an attribute with a name that is computed from a "prefix:localName" tag name and a set of prefix mappings. /// public void WriteStartAttributeComputed(string tagName, int prefixMappingsIndex) { WriteStartComputed(XPathNodeType.Attribute, tagName, prefixMappingsIndex); } /// /// Write an attribute with a name that is computed from a "prefix:localName" tag name and a namespace URI. /// public void WriteStartAttributeComputed(string tagName, string ns) { WriteStartComputed(XPathNodeType.Attribute, tagName, ns); } /// /// Write an attribute with a name that is copied from the navigator. /// public void WriteStartAttributeComputed(XPathNavigator navigator) { WriteStartComputed(XPathNodeType.Attribute, navigator); } /// /// Write an attribute with a name that is derived from the XmlQualifiedName. /// public void WriteStartAttributeComputed(XmlQualifiedName name) { WriteStartComputed(XPathNodeType.Attribute, name); } /// /// Before calling XmlRawWriter.WriteNamespaceDeclaration(), perform various checks to ensure well-formedness. /// public void WriteNamespaceDeclaration(string prefix, string ns) { string nsExisting; Debug.Assert(prefix != null && ns != null); ConstructInEnumAttrs(XPathNodeType.Namespace); if (this.nsmgr == null) { // If namespace manager has not yet been created, then there is no possibility of conflict WriteNamespaceDeclarationUnchecked(prefix, ns); } else { nsExisting = this.nsmgr.LookupNamespace(prefix); if (ns != nsExisting) { // prefix = "", ns = "", nsExisting --> Look for xmlns="", found xmlns="foo" // prefix = "", ns, nsExisting = null --> Look for xmlns="uri", no uri found // prefix = "", ns, nsExisting = "" --> Look for xmlns="uri", found xmlns="" // prefix = "", ns, nsExisting --> Look for xmlns="uri", found xmlns="uri2" // prefix, ns, nsExisting = null --> Look for xmlns:foo="uri", no uri found // prefix, ns, nsExisting --> Look for xmlns:foo="uri", found xmlns:foo="uri2" // If the prefix is mapped to a uri, if (nsExisting != null) { // Then throw an error except if the prefix trying to redefine is already used if (usedPrefixes.ContainsKey(prefix)) { throw new XslTransformException(Res.XmlIl_NmspConflict, new string[] { prefix.Length == 0 ? "" : ":", prefix, ns, nsExisting }); } } // Add namespace to manager and write it to output AddNamespace(prefix, ns); } } if (this.depth == 0) EndTree(); usedPrefixes[prefix] = ns; } /// /// Before writing a namespace, perform various checks to ensure well-formedness. /// public void WriteStartNamespace(string prefix) { Debug.Assert(prefix != null, "Invalid argument"); // Handle namespace attributes that are not sent directly to WriteNamespaceDeclaration ConstructInEnumAttrs(XPathNodeType.Namespace); this.piTarget/*nmspPrefix*/ = prefix; this.nodeText.Clear(); this.xstate = XmlState.WithinNmsp; this.depth++; } /// /// Cache the namespace's text. /// public void WriteNamespaceString(string text) { Debug.Assert(this.xstate == XmlState.WithinNmsp, "WriteNamespaceString cannot be called in the " + this.xstate + " state."); this.nodeText.ConcatNoDelimiter(text); } /// /// Before writing a namespace, perform various checks to ensure well-formedness. /// public void WriteEndNamespace() { Debug.Assert(this.xstate == XmlState.WithinNmsp, "WriteEndNamespace cannot be called in the " + this.xstate + " state."); this.xstate = XmlState.EnumAttrs; this.depth--; // Write cached namespace attribute WriteNamespaceDeclaration(this.piTarget/*nmspPrefix*/, this.nodeText.GetResult()); if (this.depth == 0) EndTree(); } /// /// Before writing a comment, perform various checks to ensure well-formedness. /// public void WriteStartComment() { // Xml state transitions ConstructWithinContent(XPathNodeType.Comment); this.nodeText.Clear(); this.xstate = XmlState.WithinComment; this.depth++; } /// /// Cache the comment's text. /// public void WriteCommentString(string text) { Debug.Assert(this.xstate == XmlState.WithinComment, "WriteCommentString cannot be called in the " + this.xstate + " state."); this.nodeText.ConcatNoDelimiter(text); } /// /// Before writing a comment, perform various checks to ensure well-formedness. /// public void WriteEndComment() { Debug.Assert(this.xstate == XmlState.WithinComment, "WriteEndComment cannot be called in the " + this.xstate + " state."); Writer.WriteComment(this.nodeText.GetResult()); this.xstate = XmlState.WithinContent; this.depth--; if (this.depth == 0) EndTree(); } /// /// Before writing a processing instruction, perform various checks to ensure well-formedness. /// public void WriteStartProcessingInstruction(string target) { // Xml state transitions ConstructWithinContent(XPathNodeType.ProcessingInstruction); // Verify PI name ValidateNames.ValidateNameThrow("", target, "", XPathNodeType.ProcessingInstruction, ValidateNames.Flags.AllExceptPrefixMapping); this.piTarget = target; this.nodeText.Clear(); this.xstate = XmlState.WithinPI; this.depth++; } /// /// Cache the processing instruction's text. /// public void WriteProcessingInstructionString(string text) { Debug.Assert(this.xstate == XmlState.WithinPI, "WriteProcessingInstructionString cannot be called in the " + this.xstate + " state."); this.nodeText.ConcatNoDelimiter(text); } /// /// Before writing a processing instruction, perform various checks to ensure well-formedness. /// public void WriteEndProcessingInstruction() { Debug.Assert(this.xstate == XmlState.WithinPI, "WriteEndProcessingInstruction cannot be called in the " + this.xstate + " state."); Writer.WriteProcessingInstruction(this.piTarget, this.nodeText.GetResult()); this.xstate = XmlState.WithinContent; this.depth--; // Xml state transitions if (this.depth == 0) EndTree(); } /// /// Write an item to output. If currently constructing an Xml tree, then the item is always copied. /// At the top-level, the item's identity is preserved unless it's an atomic value. /// public void WriteItem(XPathItem item) { if (item.IsNode) { XPathNavigator navigator = (XPathNavigator) item; // If this is a top-level node, write a reference to it; else copy it by value if (this.xstate == XmlState.WithinSequence) this.seqwrt.WriteItem(navigator); else CopyNode(navigator); } else { // Call WriteItem for atomic values Debug.Assert(this.xstate == XmlState.WithinSequence, "Values can only be written at the top-level."); this.seqwrt.WriteItem(item); } } /// /// Copy a node by value to output according to Xslt rules: /// 1. Identity is never preserved /// 2. If the item is an Rtf, preserve serialization hints when copying. /// 3. If the item is a Root node, copy the children of the Root /// public void XsltCopyOf(XPathNavigator navigator) { RtfNavigator navRtf = navigator as RtfNavigator; if (navRtf != null) { // Copy Rtf navRtf.CopyToWriter(this); } else if (navigator.NodeType == XPathNodeType.Root) { // Copy children of root if (navigator.MoveToFirstChild()) { do { CopyNode(navigator); } while (navigator.MoveToNext()); navigator.MoveToParent(); } } else { // Copy node CopyNode(navigator); } } /// /// Begin shallow copy of the navigator's current node to output. Returns true if EndCopy /// should be called to complete the copy operation. /// Automatically copies all in-scope namespaces on elements. /// public bool StartCopy(XPathNavigator navigator) { // StartDocument is a no-op if (navigator.NodeType == XPathNodeType.Root) return true; if (StartCopy(navigator, true)) { Debug.Assert(navigator.NodeType == XPathNodeType.Element, "StartCopy should return true only for Element nodes."); // Copy namespaces to output CopyNamespaces(navigator, XPathNamespaceScope.ExcludeXml); return true; } return false; } /// /// End shallow copy of the navigator's current node. Should be called only for Element and Document nodes. /// public void EndCopy(XPathNavigator navigator) { if (navigator.NodeType == XPathNodeType.Element) WriteEndElement(); else Debug.Assert(navigator.NodeType == XPathNodeType.Root, "EndCopy should only be called for Element and Document nodes."); } //----------------------------------------------- // Helper methods //----------------------------------------------- /// /// Add an in-scope namespace. /// private void AddNamespace(string prefix, string ns) { this.nsmgr.AddNamespace(prefix, ns); this.cntNmsp++; usedPrefixes[prefix] = ns; } /// /// Before writing text, perform various checks to ensure well-formedness. /// private void WriteString(string text, bool disableOutputEscaping) { Debug.Assert(text != null, "Invalid argument"); // Xml state transitions switch (this.xstate) { case XmlState.WithinSequence: // Start constructing new tree StartTree(XPathNodeType.Text); goto case XmlState.WithinContent; case XmlState.WithinContent: if (disableOutputEscaping) WriteRawUnchecked(text); else WriteStringUnchecked(text); break; case XmlState.EnumAttrs: // Enumerating attributes, so write text as element content StartElementContentUnchecked(); goto case XmlState.WithinContent; case XmlState.WithinAttr: WriteStringUnchecked(text); break; case XmlState.WithinNmsp: WriteNamespaceString(text); break; case XmlState.WithinComment: // Comment text WriteCommentString(text); break; case XmlState.WithinPI: // PI text WriteProcessingInstructionString(text); break; default: Debug.Assert(false, "Text cannot be output in the " + this.xstate + " state."); break; } if (this.depth == 0) EndTree(); } /// /// Deep copy the subtree that is rooted at this navigator's current position to output. If the current /// item is an element, copy all in-scope namespace nodes. /// private void CopyNode(XPathNavigator navigator) { XPathNodeType nodeType; int depthStart = this.depth; Debug.Assert(navigator != null); while (true) { if (StartCopy(navigator, this.depth == depthStart)) { nodeType = navigator.NodeType; Debug.Assert(nodeType == XPathNodeType.Element, "StartCopy should return true only for Element nodes."); // Copy attributes if (navigator.MoveToFirstAttribute()) { do { StartCopy(navigator, false); } while (navigator.MoveToNextAttribute()); navigator.MoveToParent(); } // Copy namespaces in document order (navigator returns them in reverse document order) CopyNamespaces(navigator, (this.depth - 1 == depthStart) ? XPathNamespaceScope.ExcludeXml : XPathNamespaceScope.Local); StartElementContentUnchecked(); // If children exist, move down to next level if (navigator.MoveToFirstChild()) continue; EndCopy(navigator, (this.depth - 1) == depthStart); } // No children while (true) { if (this.depth == depthStart) { // The entire subtree has been copied return; } if (navigator.MoveToNext()) { // Found a sibling, so break to outer loop break; } // No siblings, so move up to previous level navigator.MoveToParent(); EndCopy(navigator, (this.depth - 1) == depthStart); } } } /// /// Begin shallow copy of the navigator's current node to output. Returns true if EndCopy /// should be called to complete the copy operation. /// private bool StartCopy(XPathNavigator navigator, bool callChk) { bool mayHaveChildren = false; switch (navigator.NodeType) { case XPathNodeType.Element: // If checks need to be made, call XmlQueryOutput.WriteStartElement if (callChk) { WriteStartElement(navigator.Prefix, navigator.LocalName, navigator.NamespaceURI); } else { WriteStartElementUnchecked(navigator.Prefix, navigator.LocalName, navigator.NamespaceURI); } mayHaveChildren = true; break; case XPathNodeType.Attribute: // If checks need to be made, call XmlQueryOutput.WriteStartAttribute if (callChk) { WriteStartAttribute(navigator.Prefix, navigator.LocalName, navigator.NamespaceURI); } else { WriteStartAttributeUnchecked(navigator.Prefix, navigator.LocalName, navigator.NamespaceURI); } // Write attribute text WriteString(navigator.Value); // If checks need to be made, call XmlQueryOutput.WriteEndAttribute if (callChk) { WriteEndAttribute(); } else { WriteEndAttributeUnchecked(); } break; case XPathNodeType.Namespace: // If checks need to be made, call XmlQueryOutput.WriteNamespaceDeclaration if (callChk) { // Do not allow namespaces to be copied after attributes XmlAttributeCache attrCache = Writer as XmlAttributeCache; if (attrCache != null && attrCache.Count != 0) throw new XslTransformException(Res.XmlIl_NmspAfterAttr, string.Empty); WriteNamespaceDeclaration(navigator.LocalName, navigator.Value); } else { WriteNamespaceDeclarationUnchecked(navigator.LocalName, navigator.Value); } break; case XPathNodeType.Text: case XPathNodeType.SignificantWhitespace: case XPathNodeType.Whitespace: // If checks need to be made, call XmlQueryOutput.WriteString if (callChk) { WriteString(navigator.Value, false); } else { // No flags are set, so this is simple element text (attributes, comments, pi's copy own text) WriteStringUnchecked(navigator.Value); } break; case XPathNodeType.Root: // Document node is invalid except at the top-level Debug.Assert(this.xstate != XmlState.WithinSequence, "StartCopy should not called if state is WithinSequence"); ThrowInvalidStateError(XPathNodeType.Root); break; case XPathNodeType.Comment: WriteStartComment(); WriteCommentString(navigator.Value); WriteEndComment(); break; case XPathNodeType.ProcessingInstruction: WriteStartProcessingInstruction(navigator.LocalName); WriteProcessingInstructionString(navigator.Value); WriteEndProcessingInstruction(); break; default: Debug.Assert(false); break; } return mayHaveChildren; } /// /// End shallow copy of the navigator's current node to output. This method should only be called if StartCopy /// returned true. /// private void EndCopy(XPathNavigator navigator, bool callChk) { Debug.Assert(navigator.NodeType == XPathNodeType.Element); Debug.Assert(this.xstate == XmlState.WithinContent, "EndCopy cannot be called in the " + this.xstate + " state."); if (callChk) WriteEndElement(); else WriteEndElementUnchecked(navigator.Prefix, navigator.LocalName, navigator.NamespaceURI); } /// /// Copy all namespaces of the specified type (in-scope, exclude-xml, local) in document order to output. /// private void CopyNamespaces(XPathNavigator navigator, XPathNamespaceScope nsScope) { Debug.Assert(navigator.NodeType == XPathNodeType.Element, "Only elements have namespaces to copy"); // Default namespace undeclaration isn't included in navigator's namespace list, so add it now if (navigator.NamespaceURI.Length == 0) { Debug.Assert(navigator.LocalName.Length != 0, "xmlns:foo='' isn't allowed"); WriteNamespaceDeclarationUnchecked(string.Empty, string.Empty); } // Since the namespace list is arranged in reverse-document order, recursively reverse it. if (navigator.MoveToFirstNamespace(nsScope)) { CopyNamespacesHelper(navigator, nsScope); navigator.MoveToParent(); } } /// /// Recursive helper function that reverses order of the namespaces retrieved by MoveToFirstNamespace and /// MoveToNextNamespace. /// private void CopyNamespacesHelper(XPathNavigator navigator, XPathNamespaceScope nsScope) { string prefix = navigator.LocalName; string ns = navigator.Value; if (navigator.MoveToNextNamespace(nsScope)) CopyNamespacesHelper(navigator, nsScope); // No possibility for conflict, since we're copying namespaces from well-formed element WriteNamespaceDeclarationUnchecked(prefix, ns); } /// /// Ensure that state transitions to WithinContent. /// private void ConstructWithinContent(XPathNodeType rootType) { Debug.Assert(rootType == XPathNodeType.Element || rootType == XPathNodeType.Comment || rootType == XPathNodeType.ProcessingInstruction); switch (this.xstate) { case XmlState.WithinSequence: // If state is WithinSequence, call XmlSequenceWriter.StartTree StartTree(rootType); this.xstate = XmlState.WithinContent; break; case XmlState.WithinContent: // Already within element content break; case XmlState.EnumAttrs: // Start element content StartElementContentUnchecked(); break; default: // Construction is not allowed in this state ThrowInvalidStateError(rootType); break; } } /// /// Ensure that state transitions to EnumAttrs. /// private void ConstructInEnumAttrs(XPathNodeType rootType) { Debug.Assert(rootType == XPathNodeType.Attribute || rootType == XPathNodeType.Namespace); switch (this.xstate) { case XmlState.WithinSequence: StartTree(rootType); this.xstate = XmlState.EnumAttrs; break; case XmlState.EnumAttrs: // Already in EnumAttrs state break; default: // Construction is not allowed in this state ThrowInvalidStateError(rootType); break; } } /// /// Namespace declarations are added to this.nsmgr. Just before element content has begun, write out /// all namespaces that were declared locally on the element. /// private void WriteCachedNamespaces() { string prefix, ns; while (this.cntNmsp != 0) { // Output each prefix->ns mapping pair Debug.Assert(this.nsmgr != null); this.cntNmsp--; this.nsmgr.GetNamespaceDeclaration(this.cntNmsp, out prefix, out ns); Writer.WriteNamespaceDeclaration(prefix, ns); } } /// /// Return the type of node that is under construction given the specified XmlState. /// private XPathNodeType XmlStateToNodeType(XmlState xstate) { switch (xstate) { case XmlState.EnumAttrs: return XPathNodeType.Element; case XmlState.WithinContent: return XPathNodeType.Element; case XmlState.WithinAttr: return XPathNodeType.Attribute; case XmlState.WithinComment: return XPathNodeType.Comment; case XmlState.WithinPI: return XPathNodeType.ProcessingInstruction; } Debug.Assert(false, xstate.ToString() + " is not a valid XmlState."); return XPathNodeType.Element; } /// /// If attribute's prefix conflicts with other prefixes then redeclare the prefix. If the prefix has /// not yet been declared, then add it to the namespace manager. /// private string CheckAttributePrefix(string prefix, string ns) { string nsExisting; Debug.Assert(prefix.Length != 0 && ns.Length != 0); // Ensure that this attribute's prefix does not conflict with previously declared prefixes in this scope if (this.nsmgr == null) { // If namespace manager has no namespaces, then there is no possibility of conflict WriteNamespaceDeclarationUnchecked(prefix, ns); } else { while (true) { // If prefix is already mapped to a different namespace, nsExisting = this.nsmgr.LookupNamespace(prefix); if (nsExisting != ns) { // Then if the prefix is already mapped, if (nsExisting != null) { // Then there is a conflict that must be resolved by finding another prefix // Always find a new prefix, even if the conflict didn't occur in the current scope // This decision allows more aggressive namespace analysis at compile-time prefix = RemapPrefix(prefix, ns, false); continue; } // Add the mapping to the current scope AddNamespace(prefix, ns); } break; } } return prefix; } /// /// Remaps an element or attribute prefix using the following rules: /// /// 1. If another in-scope prefix is already mapped to "ns", then use that /// 2. Otherwise, if a prefix was previously mapped to "ns" by this method, then use that /// 3. Otherwise, generate a new prefix of the form 'xp_??', where ?? is a stringized counter /// /// These rules tend to reduce the number of unique prefixes used throughout the tree. /// private string RemapPrefix(string prefix, string ns, bool isElemPrefix) { string genPrefix; Debug.Assert(prefix != null && ns != null && ns.Length != 0); if (this.conflictPrefixes == null) this.conflictPrefixes = new Dictionary(16); if (this.nsmgr == null) { this.nsmgr = new XmlNamespaceManager(this.runtime.NameTable); this.nsmgr.PushScope(); } // Rule #1: If another in-scope prefix is already mapped to "ns", then use that genPrefix = this.nsmgr.LookupPrefix(ns); if (genPrefix != null) { // Can't use an empty prefix for an attribute if (isElemPrefix || genPrefix.Length != 0) goto ReturnPrefix; } // Rule #2: Otherwise, if a prefix was previously mapped to "ns" by this method, then use that // Make sure that any previous prefix is different than "prefix" if (this.conflictPrefixes.TryGetValue(ns, out genPrefix) && genPrefix != prefix) { // Can't use an empty prefix for an attribute if (isElemPrefix || genPrefix.Length != 0) goto ReturnPrefix; } // Rule #3: Otherwise, generate a new prefix of the form 'xp_??', where ?? is a stringized counter genPrefix = "xp_" + (this.prefixIndex++).ToString(CultureInfo.InvariantCulture); ReturnPrefix: // Save generated prefix so that it can be possibly be reused later this.conflictPrefixes[ns] = genPrefix; return genPrefix; } /// /// Write an element or attribute with a name that is computed from a "prefix:localName" tag name and a set of prefix mappings. /// private void WriteStartComputed(XPathNodeType nodeType, string tagName, int prefixMappingsIndex) { string prefix, localName, ns; // Parse the tag name and map the prefix to a namespace runtime.ParseTagName(tagName, prefixMappingsIndex, out prefix, out localName, out ns); // Validate the name parts prefix = EnsureValidName(prefix, localName, ns, nodeType); if (nodeType == XPathNodeType.Element) WriteStartElement(prefix, localName, ns); else WriteStartAttribute(prefix, localName, ns); } /// /// Write an element or attribute with a name that is computed from a "prefix:localName" tag name and a namespace URI. /// private void WriteStartComputed(XPathNodeType nodeType, string tagName, string ns) { string prefix, localName; // Parse the tagName as a prefix, localName pair ValidateNames.ParseQNameThrow(tagName, out prefix, out localName); // Validate the name parts prefix = EnsureValidName(prefix, localName, ns, nodeType); if (nodeType == XPathNodeType.Element) WriteStartElement(prefix, localName, ns); else WriteStartAttribute(prefix, localName, ns); } /// /// Write an element or attribute with a name that is copied from the navigator. /// private void WriteStartComputed(XPathNodeType nodeType, XPathNavigator navigator) { string prefix, localName, ns; prefix = navigator.Prefix; localName = navigator.LocalName; ns = navigator.NamespaceURI; if (navigator.NodeType != nodeType) { // Validate the name parts prefix = EnsureValidName(prefix, localName, ns, nodeType); } if (nodeType == XPathNodeType.Element) WriteStartElement(prefix, localName, ns); else WriteStartAttribute(prefix, localName, ns); } /// /// Write an element or attribute with a name that is derived from the XmlQualifiedName. /// private void WriteStartComputed(XPathNodeType nodeType, XmlQualifiedName name) { string prefix; Debug.Assert(ValidateNames.ParseNCName(name.Name, 0) == name.Name.Length); // Validate the name parts prefix = (name.Namespace.Length != 0) ? RemapPrefix(string.Empty, name.Namespace, nodeType == XPathNodeType.Element) : string.Empty; prefix = EnsureValidName(prefix, name.Name, name.Namespace, nodeType); if (nodeType == XPathNodeType.Element) WriteStartElement(prefix, name.Name, name.Namespace); else WriteStartAttribute(prefix, name.Name, name.Namespace); } /// /// Ensure that the specified name parts are valid according to Xml 1.0 and Namespace 1.0 rules. Try to remap /// the prefix in order to attain validity. Throw if validity is not possible. Otherwise, return the (possibly /// remapped) prefix. /// private string EnsureValidName(string prefix, string localName, string ns, XPathNodeType nodeType) { if (!ValidateNames.ValidateName(prefix, localName, ns, nodeType, ValidateNames.Flags.AllExceptNCNames)) { // Name parts are not valid as is. Try to re-map the prefix. prefix = (ns.Length != 0) ? RemapPrefix(string.Empty, ns, nodeType == XPathNodeType.Element) : string.Empty; // Throw if validation does not work this time ValidateNames.ValidateNameThrow(prefix, localName, ns, nodeType, ValidateNames.Flags.AllExceptNCNames); } return prefix; } /// /// Push element name parts onto the stack. /// private void PushElementNames(string prefix, string localName, string ns) { // Push the name parts onto a stack if (this.stkNames == null) this.stkNames = new Stack(15); this.stkNames.Push(prefix); this.stkNames.Push(localName); this.stkNames.Push(ns); } /// /// Pop element name parts from the stack. /// private void PopElementNames(out string prefix, out string localName, out string ns) { Debug.Assert(this.stkNames != null); ns = this.stkNames.Pop(); localName = this.stkNames.Pop(); prefix = this.stkNames.Pop(); } /// /// Throw an invalid state transition error. /// private void ThrowInvalidStateError(XPathNodeType constructorType) { switch (constructorType) { case XPathNodeType.Element: case XPathNodeType.Root: case XPathNodeType.Text: case XPathNodeType.Comment: case XPathNodeType.ProcessingInstruction: throw new XslTransformException(Res.XmlIl_BadXmlState, new string[] {constructorType.ToString(), XmlStateToNodeType(this.xstate).ToString()}); case XPathNodeType.Attribute: case XPathNodeType.Namespace: if (this.depth == 1) throw new XslTransformException(Res.XmlIl_BadXmlState, new string[] {constructorType.ToString(), this.rootType.ToString()}); if (this.xstate == XmlState.WithinContent) throw new XslTransformException(Res.XmlIl_BadXmlStateAttr, string.Empty); goto case XPathNodeType.Element; default: throw new XslTransformException(Res.XmlIl_BadXmlState, new string[] {"Unknown", XmlStateToNodeType(this.xstate).ToString()}); } } } }