|
// 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.
using MS.Internal;
using System.Xml;
using System.Windows.Markup; // TypeConvertContext, ParserContext
using System.Windows.Controls;
using System.Globalization;
//
// Description: Set of static methods implementing text range serialization
//
namespace System.Windows.Documents
{
/// <summary>
/// TextRangeSerialization is a static class containing
/// an implementation for TextRange serialization functionality.
/// It is only used from TextRange.GetXml/AppendXml methods.
/// </summary>
internal static class TextRangeSerialization
{
// -------------------------------------------------------------
//
// Internal Methods
//
// -------------------------------------------------------------
#region Internal Methods
internal static void WriteXaml(XmlWriter xmlWriter, ITextRange range, bool useFlowDocumentAsRoot, WpfPayload wpfPayload)
{
WriteXaml(xmlWriter, range, useFlowDocumentAsRoot, wpfPayload, false);
}
/// <summary>
/// Writes a content of current range in form of valid xml.
/// Places an artificial element xaml:FlowDocument as a root of output xml.
/// </summary>
/// <param name="xmlWriter">
/// XmlWriter to which the range will be serialized
/// </param>
/// <param name="range">
/// TextRange whose content is copied into XmlWriter xmlWriter.
/// </param>
/// <param name="useFlowDocumentAsRoot">
/// true means that we need to serialize the whole FlowDocument - used in FileSave scenario;
/// false means that we are in copy-paste scenario and will use Section or Span as a root - depending on context.
/// </param>
/// <param name="wpfPayload">
/// When this parameter is not null, images are serialized. When null, images are stripped out.
/// </param>
/// <param name="preserveTextElements">
/// When TRUE, TextElements are serialized as-is. When FALSE, they're upcast to their base type.
/// </param>
internal static void WriteXaml(XmlWriter xmlWriter, ITextRange range, bool useFlowDocumentAsRoot, WpfPayload wpfPayload, bool preserveTextElements)
{
// Set unindented formatting to avoid inserting insignificant whitespaces as significant ones
Formatting saveWriterFormatting = Formatting.None;
if (xmlWriter is XmlTextWriter)
{
saveWriterFormatting = ((XmlTextWriter)xmlWriter).Formatting;
((XmlTextWriter)xmlWriter).Formatting = Formatting.None;
}
// Get the default xamlTypeMapper.
XamlTypeMapper xamlTypeMapper = XmlParserDefaults.DefaultMapper;
// Identify structural scope of selection - nearest common ancestor
ITextPointer commonAncestor = FindSerializationCommonAncestor(range);
// Decide whether we need last paragraph merging or not
bool lastParagraphMustBeMerged =
!TextPointerBase.IsAfterLastParagraph(range.End) &&
range.End.GetPointerContext(LogicalDirection.Backward) != TextPointerContext.ElementStart;
// Write wrapping element with contextual properties
WriteRootFlowDocument(range, commonAncestor, xmlWriter, xamlTypeMapper, lastParagraphMustBeMerged, useFlowDocumentAsRoot);
// The ignoreWriteHyperlinkEnd flag will be set after call WriteOpeningTags.
// If ignoreWriteHyperlinkEnd is true, WriteXamlTextSegment won't write Hyperlink end element
// since Hyperlink writing opening tag is ignored by selecting the partial of Hyperlink.
bool ignoreWriteHyperlinkEnd;
List<int> ignoreList = new List<int>();
// Start counting tags needed to be closed.
// EmptyDocumentDepth==1 - counts FlowDocument opened in WriteRootFlowDocument above.
int elementLevel = EmptyDocumentDepth + WriteOpeningTags(range, range.Start, commonAncestor, xmlWriter, xamlTypeMapper, /*reduceElement:*/wpfPayload == null, out ignoreWriteHyperlinkEnd, ref ignoreList, preserveTextElements);
if (range.IsTableCellRange)
{
WriteXamlTableCellRange(xmlWriter, range, xamlTypeMapper, ref elementLevel, wpfPayload, preserveTextElements);
}
else
{
WriteXamlTextSegment(xmlWriter, range.Start, range.End, xamlTypeMapper, ref elementLevel, wpfPayload, ignoreWriteHyperlinkEnd, ignoreList, preserveTextElements);
}
// Close all remaining tags - scoping its End position
Invariant.Assert(elementLevel >= 0, "elementLevel cannot be negative");
while (elementLevel-- > 0)
{
xmlWriter.WriteFullEndElement();
}
// Restore xmlWriter's Formatting property
if (xmlWriter is XmlTextWriter)
{
((XmlTextWriter)xmlWriter).Formatting = saveWriterFormatting;
}
}
/// <summary>
/// Reads a well-formed xml representing a serialized text range.
/// It expects a root element xaml:FlowDocument and two range markers
/// The result of reading is pasting this text into End position
/// of text range.
/// </summary>
/// <param name="range">
/// TextRange designating the target position for pasting.
/// The existing content of a range will be deleted and new content
/// will be inserted at the end.
/// Resulting locations of Start/End positions depend on their gravities.
/// Normally (when gravity=Backward/Forward respectively) the resulting
/// range will embrace the inserted content.
/// </param>
/// <param name="fragment">
/// Represents a portion of xml to insert into the range.
/// </param>
/// <remarks>
/// We are expecting to be called with xmlReader on opening tag
/// of root text range element - xaml:FlowDocument.
/// Some insignificant stuff may occur before the root though.
/// Otherwise exception will be thrown.
/// </remarks>
internal static void PasteXml(TextRange range, TextElement fragment)
{
Invariant.Assert(fragment != null);
// Check a special case for pasing a single embedded element
if (PasteSingleEmbeddedElement(range, fragment))
{
// All done. Return successfully.
return;
}
// Set default value for an indicator of whether we need to merge last paragraph or not.
// It depends on a state of a range, so do it before emptying the range.
AdjustFragmentForTargetRange(fragment, range);
// Delete current content of a range
if (!range.IsEmpty)
{
range.Text = String.Empty;
}
Invariant.Assert(range.IsEmpty, "range must be empty in the beginning of pasting");
// Chek special case of empty pasted fragment
if (((ITextPointer)fragment.ContentStart).CompareTo(fragment.ContentEnd) == 0)
{
// Pasted fragment is empty. Nothing to insert.
return;
}
// Transfer the content from reader to writer and merge elements on both ends
PasteTextFragment(fragment, range);
}
#endregion Internal Methods
// -------------------------------------------------------------
//
// Private Methods
//
// -------------------------------------------------------------
#region Private Methods
// .............................................................
//
// Serialization
//
// .............................................................
/// <summary>
/// This function serializes text segment formed by rangeStart and rangeEnd to valid xml using xmlWriter.
/// </summary>
private static void WriteXamlTextSegment(XmlWriter xmlWriter, ITextPointer rangeStart, ITextPointer rangeEnd, XamlTypeMapper xamlTypeMapper, ref int elementLevel, WpfPayload wpfPayload, bool ignoreWriteHyperlinkEnd, List<int> ignoreList, bool preserveTextElements)
{
// Special case for pure text selection - we need a Run wrapper for it.
if (elementLevel == EmptyDocumentDepth && typeof(Run).IsAssignableFrom(rangeStart.ParentType))
{
elementLevel++;
xmlWriter.WriteStartElement(typeof(Run).Name);
}
// Create text navigator for reading the range's content
ITextPointer textReader = rangeStart.CreatePointer();
// Exclude last opening tag from serialization - we don't need to create extra element
// is cases when we have whole paragraphs/cells selected.
// NOTE: We do this slightly differently than in TextRangeEdit.AdjustRangeEnd, where we use normalization for adjusted position.
// In this case normalized position does not work, because we need to keep information about crossed paragraph boundary.
while (rangeEnd.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart)
{
rangeEnd = rangeEnd.GetNextContextPosition(LogicalDirection.Backward);
}
// Write the range internal contents
while (textReader.CompareTo(rangeEnd) < 0)
{
TextPointerContext runType = textReader.GetPointerContext(LogicalDirection.Forward);
switch (runType)
{
case TextPointerContext.ElementStart:
TextElement nextElement = (TextElement)textReader.GetAdjacentElement(LogicalDirection.Forward);
if (nextElement is Hyperlink)
{
// Don't write Hyperlink start element if Hyperlink is invalid
// in case of having a UiElement except Image or stated the range end
// position before the end position of the Hyperlink.
if (IsHyperlinkInvalid(textReader, rangeEnd))
{
ignoreWriteHyperlinkEnd = true;
textReader.MoveToNextContextPosition(LogicalDirection.Forward);
continue;
}
}
else if (nextElement != null)
{
// This code is a generic version of the more specific Hyperlink code
// directly above. That code should be folded into this.
TextElementEditingBehaviorAttribute att = (TextElementEditingBehaviorAttribute)Attribute.GetCustomAttribute(nextElement.GetType(), typeof(TextElementEditingBehaviorAttribute));
if (att != null && !att.IsTypographicOnly)
{
if (IsPartialNonTypographic(textReader, rangeEnd))
{
// Add pointer to ignore list
ITextPointer ptr = textReader.CreatePointer();
ptr.MoveToElementEdge(ElementEdge.BeforeEnd);
ignoreList.Add(ptr.Offset);
textReader.MoveToNextContextPosition(LogicalDirection.Forward);
continue;
}
}
}
elementLevel++;
textReader.MoveToNextContextPosition(LogicalDirection.Forward);
WriteStartXamlElement(/*range:*/null, textReader, xmlWriter, xamlTypeMapper, /*reduceElement:*/wpfPayload == null, preserveTextElements);
break;
case TextPointerContext.ElementEnd:
// Don't write Hyperlink end element if Hyperlink include the invalid
// in case of having a UiElement except Image or stated the range end
// before the end position of the Hyperlink or Hyperlink opening tag is
// skipped from WriteOpeningTags by selecting of the partial of Hyperlink.
if (ignoreWriteHyperlinkEnd && (textReader.GetAdjacentElement(LogicalDirection.Forward) is Hyperlink))
{
// Reset the flag to keep walk up the next Hyperlink tag
ignoreWriteHyperlinkEnd = false;
textReader.MoveToNextContextPosition(LogicalDirection.Forward);
continue;
}
// Check the ignore list
if (ignoreList.Count > 0)
{
ITextPointer endPointer = textReader.CreatePointer();
endPointer.MoveToElementEdge(ElementEdge.BeforeEnd); // Is this necessary?
if (ignoreList.Contains(endPointer.Offset))
{
ignoreList.Remove(endPointer.Offset);
textReader.MoveToNextContextPosition(LogicalDirection.Forward);
continue;
}
}
elementLevel--;
if (TextSchema.IsBreak(textReader.ParentType))
{
// For LineBreak, etc. use empty element syntax
xmlWriter.WriteEndElement();
}
else
{ // No need in enforcing full element notation. Remove this branch.
// For all other textelements use explicit closing tag.
xmlWriter.WriteFullEndElement();
}
textReader.MoveToNextContextPosition(LogicalDirection.Forward);
break;
case TextPointerContext.Text:
int textLength = textReader.GetTextRunLength(LogicalDirection.Forward);
char[] text = new Char[textLength];
textLength = TextPointerBase.GetTextWithLimit(textReader, LogicalDirection.Forward, text, 0, textLength, rangeEnd);
// XmlWriter will throw an ArgumentException if text contains
// any invalid surrogates, so strip them out now.
textLength = StripInvalidSurrogateChars(text, textLength);
xmlWriter.WriteChars(text, 0, textLength);
textReader.MoveToNextContextPosition(LogicalDirection.Forward);
break;
case TextPointerContext.EmbeddedElement:
object embeddedObject = textReader.GetAdjacentElement(LogicalDirection.Forward);
textReader.MoveToNextContextPosition(LogicalDirection.Forward);
WriteEmbeddedObject(embeddedObject, xmlWriter, wpfPayload);
break;
default:
Invariant.Assert(false, "unexpected value of runType");
textReader.MoveToNextContextPosition(LogicalDirection.Forward);
break;
}
}
}
/// <summary>
/// Serializes a rectagular table range
/// </summary>
private static void WriteXamlTableCellRange(XmlWriter xmlWriter, ITextRange range, XamlTypeMapper xamlTypeMapper, ref int elementLevel, WpfPayload wpfPayload, bool preserveTextElements)
{
Invariant.Assert(range.IsTableCellRange, "range is expected to be in IsTableCellRange state");
List<TextSegment> textSegments = range.TextSegments;
int checkElementLevel = -1; // negative value as an indicator that it is not yet initialized
// Set ignoreWriteHyperlinkEnd as false initially
bool ignoreWriteHyperlinkEnd = false;
List<int> ignoreList = new List<int>();
for (int i = 0; i < textSegments.Count; i++)
{
TextSegment textSegment = textSegments[i];
// Open a row for this segment (except for the very first one, for which we opened a row in a WriteOpeningTags method)
if (i > 0)
{
ITextPointer pointer = textSegment.Start.CreatePointer();
while (!typeof(TableRow).IsAssignableFrom(pointer.ParentType))
{
Invariant.Assert(typeof(TextElement).IsAssignableFrom(pointer.ParentType), "pointer must be still in a scope of TextElement");
pointer.MoveToElementEdge(ElementEdge.BeforeStart);
}
Invariant.Assert(typeof(TableRow).IsAssignableFrom(pointer.ParentType), "pointer must be in a scope of TableRow");
pointer.MoveToElementEdge(ElementEdge.BeforeStart);
ITextRange textRange = new TextRange(textSegment.Start, textSegment.End);
elementLevel += WriteOpeningTags(textRange, textSegment.Start, pointer, xmlWriter, xamlTypeMapper, /*reduceElement:*/wpfPayload == null, out ignoreWriteHyperlinkEnd, ref ignoreList, preserveTextElements);
}
// Output the cell segment for one row
WriteXamlTextSegment(xmlWriter, textSegment.Start, textSegment.End, xamlTypeMapper, ref elementLevel, wpfPayload, ignoreWriteHyperlinkEnd, ignoreList, preserveTextElements);
Invariant.Assert(elementLevel >= 4, "At the minimun we expected to stay within four elements: Section(wrapper),Table,TableRowGroup,TableRow");
if (checkElementLevel < 0) checkElementLevel = elementLevel; // initialize level checking variable
Invariant.Assert(checkElementLevel == elementLevel, "elementLevel is supposed to be unchanged between segments of table cell range");
// Assuming that the element is TableRow - close it.
// NOTE: Such assumption is valid because WriteXamlTextSegment moves end pointer out of all opening tags,
// so it ends serialization immediately after the last cell's closing tag.
// This means that we only need to close one level - for TableRow.
// To make the code more reliable we need to add explicit control for closing elements.
elementLevel--;
xmlWriter.WriteFullEndElement();
}
}
/// <summary>
/// Walks the tree up from current position and writes all scoping tags
/// in their natural order - from root to leafs.
/// </summary>
/// <param name="range">
/// Range identifying the whole selection.
/// Needed for
/// - table cell range case: proper column processing: to output only columns related to the selection
/// - text segement case: hyperlink serialization heuristics
/// </param>
/// <param name="thisElement">
/// ITextPointer identifying an element.
/// </param>
/// <param name="scope">
/// A position identifying the scope which should be used for serialization.
/// All tags outside of this scope will be ignored.
/// </param>
/// <param name="xmlWriter">
/// XmlWriter to write element tags.
/// </param>
/// <param name="xamlTypeMapper"></param>
/// <param name="reduceElement">
/// <see cref="WriteStartXamlElement"/>
/// </param>
/// <param name="ignoreWriteHyperlinkEnd"></param>
/// <param name="ignoreList"></param>
/// <param name="preserveTextElements"></param>
/// /// <returns>
/// Number of opening tags written into XmlWriter.
/// This number should be used afterwards to close all opened tags.
/// </returns>
private static int WriteOpeningTags(ITextRange range, ITextPointer thisElement, ITextPointer scope, XmlWriter xmlWriter, XamlTypeMapper xamlTypeMapper, bool reduceElement, out bool ignoreWriteHyperlinkEnd, ref List<int> ignoreList, bool preserveTextElements)
{
ignoreWriteHyperlinkEnd = false;
// Recursion ends when we reach the scope level. We will write tags on returing path from the recursion
if (thisElement.HasEqualScope(scope))
{
return 0; // no elements have opened at this level. Return elementCount==0.
}
Invariant.Assert(typeof(TextElement).IsAssignableFrom(thisElement.ParentType), "thisElement is expected to be a TextElement");
ITextPointer previousLevel = thisElement.CreatePointer();
previousLevel.MoveToElementEdge(ElementEdge.BeforeStart);
// Recurse into the parent element
int elementLevel = WriteOpeningTags(range, previousLevel, scope, xmlWriter, xamlTypeMapper, reduceElement, out ignoreWriteHyperlinkEnd, ref ignoreList, preserveTextElements);
// After returning from the recursion - when all parent tags have been written,
// write the opening tag for this element
// Hyperlink open tag will be skipped since the range selection of Hyperlink is the partial
// of Hyperlink range or Hyperlink include invalid UIElement except Image.
bool ignoreHyperlink = false;
bool isPartialNonTypographic = false;
if (thisElement.ParentType == typeof(Hyperlink))
{
if (TextPointerBase.IsAtNonMergeableInlineStart(range.Start))
{
ITextPointer position = thisElement.CreatePointer();
position.MoveToElementEdge(ElementEdge.BeforeStart);
ignoreHyperlink = IsHyperlinkInvalid(position, range.End);
}
else
{
ignoreHyperlink = true;
}
}
else
{
// This code is a generic version of the more specific Hyperlink code
// directly above. That code should be folded into this.
TextElementEditingBehaviorAttribute att = (TextElementEditingBehaviorAttribute)Attribute.GetCustomAttribute(thisElement.ParentType, typeof(TextElementEditingBehaviorAttribute));
if (att != null && !att.IsTypographicOnly)
{
if (TextPointerBase.IsAtNonMergeableInlineStart(range.Start))
{
ITextPointer position = thisElement.CreatePointer();
position.MoveToElementEdge(ElementEdge.BeforeStart);
isPartialNonTypographic = IsPartialNonTypographic(position, range.End);
}
else
{
isPartialNonTypographic = true;
}
}
}
int count;
if (ignoreHyperlink)
{
// Ignore writing Hyperlink opening tag
ignoreWriteHyperlinkEnd = true;
// Set elementLevel without adding it
count = elementLevel;
}
else if (isPartialNonTypographic)
{
// Add the end pointer to the list
ITextPointer position = thisElement.CreatePointer();
position.MoveToElementEdge(ElementEdge.BeforeEnd);
ignoreList.Add(position.Offset);
// Set elementLevel without adding to it
count = elementLevel;
}
else
{
// Write the opening tag
WriteStartXamlElement(range, thisElement, xmlWriter, xamlTypeMapper, reduceElement, preserveTextElements);
// Each opening tag adds one to the level count
count = elementLevel + 1;
}
// Return the opening tag count
return count;
}
/// <summary>
/// Writes an opening tag of an element together with all attributes
/// representing Avalon properties.
/// </summary>
/// <param name="range">
/// Parameter used for top-level Table element - to decide what columns to output.
/// For all other elements it's ignored.
/// </param>
/// <param name="textReader">
/// TextPointer positioned in the scope of element whose
/// start tag is going to be written.
/// </param>
/// <param name="xmlWriter">
/// XmlWriter to output element opening tag.
/// </param>
/// <param name="xamlTypeMapper"></param>
/// <param name="reduceElement">
/// True value of this parameter indicates that
/// serialization goes into XamlPackage, so all elements
/// can be preserved as is; otherwise some of them must be
/// reduced into simpler representations (such as InlineUIContainer -> Run
/// and BlockUIContainer -> Paragraph).
/// </param>
/// <param name="preserveTextElements">
/// If TRUE, TextElements are serialized as-is. If FALSE, they're upcast
/// to their base types.
/// </param>
private static void WriteStartXamlElement(ITextRange range, ITextPointer textReader, XmlWriter xmlWriter, XamlTypeMapper xamlTypeMapper, bool reduceElement, bool preserveTextElements)
{
Type elementType = textReader.ParentType;
Type elementTypeStandardized = TextSchema.GetStandardElementType(elementType, reduceElement);
// Get rid f UIContainers when their child is not an image
if (elementTypeStandardized == typeof(InlineUIContainer) || elementTypeStandardized == typeof(BlockUIContainer))
{
Invariant.Assert(!reduceElement);
InlineUIContainer inlineUIContainer = textReader.GetAdjacentElement(LogicalDirection.Backward) as InlineUIContainer;
BlockUIContainer blockUIContainer = textReader.GetAdjacentElement(LogicalDirection.Backward) as BlockUIContainer;
if ((inlineUIContainer == null || !(inlineUIContainer.Child is Image)) &&
(blockUIContainer == null || !(blockUIContainer.Child is Image)))
{
// Even when we serialize for DataFormats.XamlPackage we strip out UIElement
// different from Images.
// Note that this condition is consistent with the one in WriteEmbeddedObject -
// so that when we reduce the element type fromm UIContainer to Run/Paragraph
// we also output just a space instead of the embedded object conntained in it.
elementTypeStandardized = TextSchema.GetStandardElementType(elementType, /*reduceElement:*/true);
}
}
else if (preserveTextElements)
{
elementTypeStandardized = elementType;
}
bool customTextElement = preserveTextElements && !TextSchema.IsKnownType(elementType);
if (customTextElement)
{
// If the element is not from PresentationFramework, we'll need to serialize a namespace
// Will module name always have a '.'? If so, can remove conditional in assembly assignment below
int index = elementTypeStandardized.Module.Name.LastIndexOf('.');
string assembly = (index == -1 ? elementTypeStandardized.Module.Name : elementTypeStandardized.Module.Name.Substring(0, index));
string nameSpace = $"clr-namespace:{elementTypeStandardized.Namespace};assembly={assembly}";
string prefix = elementTypeStandardized.Namespace;
xmlWriter.WriteStartElement(prefix, elementTypeStandardized.Name, nameSpace);
}
else
{
xmlWriter.WriteStartElement(elementTypeStandardized.Name);
}
// Write properties
DependencyObject complexProperties = new DependencyObject();
WriteInheritableProperties(elementTypeStandardized, textReader, xmlWriter, /*onlyAffected:*/true, complexProperties);
WriteNoninheritableProperties(elementTypeStandardized, textReader, xmlWriter, /*onlyAffected:*/true, complexProperties);
if (customTextElement)
{
WriteLocallySetProperties(elementTypeStandardized, textReader, xmlWriter, complexProperties);
}
WriteComplexProperties(xmlWriter, complexProperties, elementTypeStandardized);
// Special case for Table element serialization
if (elementTypeStandardized == typeof(Table) && textReader is TextPointer)
{
// Write the columns text.
WriteTableColumnsInformation(range, (Table)((TextPointer)textReader).Parent, xmlWriter, xamlTypeMapper);
}
}
// Write columns related to the given table cell range.
private static void WriteTableColumnsInformation(ITextRange range, Table table, XmlWriter xmlWriter, XamlTypeMapper xamlTypeMapper)
{
TableColumnCollection columns = table.Columns;
int startColumn;
int endColumn;
if (!TextRangeEditTables.GetColumnRange(range, table, out startColumn, out endColumn))
{
startColumn = 0;
endColumn = columns.Count - 1;
}
Invariant.Assert(startColumn >= 0, "startColumn index is supposed to be non-negative");
if(columns.Count > 0)
{
// Build an appropriate name for the complex property
string complexPropertyName = $"{table.GetType().Name}.Columns";
// Write the start element for the complex property.
xmlWriter.WriteStartElement(complexPropertyName);
for (int i = startColumn; i <= endColumn && i < columns.Count; i++)
{
WriteXamlAtomicElement(columns[i], xmlWriter, /*reduceElement:*/false);
}
// Close the element for the complex property
xmlWriter.WriteEndElement();
}
}
/// <summary>
/// Creates a FlowDocument element wrapping copied content and storing its contextual properties.
/// </summary>
/// <param name="range"></param>
/// <param name="context"></param>
/// <param name="xmlWriter"></param>
/// <param name="xamlTypeMapper"></param>
/// <param name="lastParagraphMustBeMerged"></param>
/// <param name="useFlowDocumentAsRoot">
/// true means that we need to serialize the whole FlowDocument - used in FileSave scenario;
/// false means that we are in copy-paste scenario and will use Section or Span as a root - depending on context.
/// </param>
private static void WriteRootFlowDocument(ITextRange range, ITextPointer context, XmlWriter xmlWriter, XamlTypeMapper xamlTypeMapper, bool lastParagraphMustBeMerged, bool useFlowDocumentAsRoot)
{
Type rootType;
const string xmlNamespace = "http://schemas.microsoft.com/winfx/2006/xaml/presentation";
const string xmlns = "xmlns";
// Decide what root element to use
if (useFlowDocumentAsRoot)
{
rootType = typeof(FlowDocument);
}
else
{
Type contextType = context.ParentType;
if (contextType == null ||
typeof(Paragraph).IsAssignableFrom(contextType) ||
typeof(Inline).IsAssignableFrom(contextType) && !typeof(AnchoredBlock).IsAssignableFrom(contextType))
{
rootType = typeof(Span);
}
else
{
rootType = typeof(Section);
}
}
// Create a root element FlowDocument
xmlWriter.WriteStartElement(rootType.Name, xmlNamespace);
// Define default namespace as Avalon namespace
xmlWriter.WriteAttributeString(xmlns, xmlNamespace);
// Set the value of xml:space to "preserve" to consider all spaces as significant
// Note that Xml treats whitespaces as significant if they belong to some nonempty line
// (neighbored by non-whitespace characters at least from one side)
// That's why we only loose whitespaces if they occupy the whole textrun in xml.
// So alternative solution for whitespace preservation could be setting xml:space="preserve"
// attribute to only empty runs - this would make our whitespace preservation more
// narrowed...
// Investigate if this is really worth doing.
// Currently we wildly preserve all whitespaces in TextRange, which can produce
// a problem if somebody manually provides this "innocent" xaml (notice newline between paragraphs):
// <Paragraph>SomeText</Paragraph>
// <Paragraph>OtherText</Paragraph>
// In pasting this with all-whitespace-preservation we will add extra new line between paragraphs.
// Formally it will be correct, but people may get confused with using our AppendXaml API.
xmlWriter.WriteAttributeString("xml:space", "preserve");
// Write all contextual properties as attributes of root fragment
DependencyObject complexProperties = new DependencyObject();
if (useFlowDocumentAsRoot)
{
WriteInheritablePropertiesForFlowDocument((DependencyObject)((TextPointer)context).Parent, xmlWriter, complexProperties);
}
else
{
WriteInheritableProperties(rootType, context, xmlWriter, /*onlyAffected:*/false, complexProperties);
}
if (rootType == typeof(Span))
{
// Root element is not real element to paste. It is just a property bag for contextual properties.
// So we collect non-inheritable properties only for inline content; not needing it for block one.
WriteNoninheritableProperties(typeof(Span), context, xmlWriter, /*onlyAffected:*/false, complexProperties);
}
// Write an indicator that last paragraph must be merged on paste
if (rootType == typeof(Section) && lastParagraphMustBeMerged)
{
xmlWriter.WriteAttributeString(Section.HasTrailingParagraphBreakOnPastePropertyName, "False");
}
// Note that we are skipping background property, because we only want to transfer it for the whole document.
// This heuristic for the "whole-document" background must be implemented.
WriteComplexProperties(xmlWriter, complexProperties, rootType);
}
private static void WriteInheritablePropertiesForFlowDocument(DependencyObject context, XmlWriter xmlWriter, DependencyObject complexProperties)
{
DependencyProperty[] inheritableProperties = TextSchema.GetInheritableProperties(typeof(FlowDocument));
for (int i = 0; i < inheritableProperties.Length; i++)
{
DependencyProperty property = inheritableProperties[i];
object value = context.ReadLocalValue(property);
if (value != DependencyProperty.UnsetValue)
{
string stringValue = DPTypeDescriptorContext.GetStringValue(property, value);
if (stringValue != null)
{
stringValue = FilterNaNStringValueForDoublePropertyType(stringValue, property.PropertyType);
string propertyName;
if (property == FrameworkContentElement.LanguageProperty)
{
// Special case for CultureInfo property that must be represented in xaml as xml:lang attribute
propertyName = "xml:lang";
}
else
{
// Regular case using own property name
propertyName = property.OwnerType == typeof(Typography) ? $"Typography.{property.Name}" : property.Name;
}
xmlWriter.WriteAttributeString(propertyName, stringValue);
}
else
{
complexProperties.SetValue(property, value);
}
}
}
}
// Writes a collection of attributes representing inheritable properties
// whose values has been affected by this element.
// Parameter onlyAffected=true means that we serialize only properties affected by
// the current element; otherwise we output all known inheritable properties.
private static void WriteInheritableProperties(Type elementTypeStandardized, ITextPointer context, XmlWriter xmlWriter, bool onlyAffected, DependencyObject complexProperties)
{
// Create a pointer positioned immediately outside the element
ITextPointer outerContext = null;
if (onlyAffected)
{
outerContext = context.CreatePointer();
outerContext.MoveToElementEdge(ElementEdge.BeforeStart);
}
DependencyProperty[] inheritableProperties = TextSchema.GetInheritableProperties(elementTypeStandardized);
for (int i = 0; i < inheritableProperties.Length; i++)
{
DependencyProperty property = inheritableProperties[i];
object innerValue = context.GetValue(property);
if (innerValue == null)
{
// Some properties like Foreground may have null as default value.
// Skip them.
continue;
}
object outerValue = null;
if (onlyAffected)
{
outerValue = outerContext.GetValue(property);
}
// The property must appear in markup if the element
if (!onlyAffected || // all properties requested for saving context on root
!TextSchema.ValuesAreEqual(innerValue, outerValue)) // or the element really affects the property
{
string stringValue = DPTypeDescriptorContext.GetStringValue(property, innerValue);
if (stringValue != null)
{
stringValue = FilterNaNStringValueForDoublePropertyType(stringValue, property.PropertyType);
string propertyName;
if (property == FrameworkContentElement.LanguageProperty)
{
// Special case for CultureInfo property that must be represented in xaml as xml:lang attribute
propertyName = "xml:lang";
}
else
{
// Regular case: serialize a property with its own name
propertyName = GetPropertyNameForElement(property, elementTypeStandardized, /*forceComplexName:*/false);
}
xmlWriter.WriteAttributeString(propertyName, stringValue);
}
else
{
complexProperties.SetValue(property, innerValue);
}
}
}
}
// Writes a collection of attributes representing non-inheritable properties
// whose values are set inline on the given element instance.
// When we read properties fromContext we want all values including defaults; from text elements we only want only affected
private static void WriteNoninheritableProperties(Type elementTypeStandardized, ITextPointer context, XmlWriter xmlWriter, bool onlyAffected, DependencyObject complexProperties)
{
DependencyProperty[] elementProperties = TextSchema.GetNoninheritableProperties(elementTypeStandardized);
// We'll need a pointer to walk the tree up when onlyAffected=false
ITextPointer parentContext = onlyAffected ? null : context.CreatePointer();
for (int i = 0; i < elementProperties.Length; i++)
{
DependencyProperty property = elementProperties[i];
Type propertyOwnerType = context.ParentType;
object propertyValue;
if (onlyAffected)
{
// This way of getting properties works only for elements whose style defaults are equal to known types' style defaults
// If user defines some element (say, Heading1) with different default properties we will replace them unintentionally to Paragraph's defaults.
propertyValue = context.GetValue(property);
}
else
{
// This is request for contextual properties - use "manual" inheritance to collect values
Invariant.Assert(elementTypeStandardized == typeof(Span), "Request for contextual properties is expected for Span wrapper only");
// Get property value from this element or from one of its ancestors (the latter in case of !onlyAffeted)
propertyValue = context.GetValue(property);
// Get property value from its ancestors if the property is not set.
// TextDecorationCollection is special-cased as its default is empty collection,
// and its value source cannot be distinguished from ITextPointer.
if (propertyValue == null || TextDecorationCollection.Empty.ValueEquals(propertyValue as TextDecorationCollection))
{
if (property == Inline.BaselineAlignmentProperty || property == TextElement.TextEffectsProperty)
{
// These properties do not make sense as contextual; do not include them into context.
continue;
}
parentContext.MoveToPosition(context);
while ((propertyValue == null || TextDecorationCollection.Empty.ValueEquals(propertyValue as TextDecorationCollection))
&& typeof(Inline).IsAssignableFrom(parentContext.ParentType))
{
parentContext.MoveToElementEdge(ElementEdge.BeforeStart);
propertyValue = parentContext.GetValue(property);
propertyOwnerType = parentContext.ParentType;
}
}
}
// Paragraph has Margin="Auto" in default style. List has Margin as well as Padding="Auto" in the style.
// This "Auto" value coming from style is different than the default value
// in the property metadata (which is zero thickness).
// So we have the following hard-coded check to avoid serializing them.
// The intention here is to not bloat the size of serialized xaml for frequently used elements.
// Note that, in doing so we are at risk of breaking our typographically equivalent serialization philosophy,
// when the target of paste operation has a different style setting.
if ((property == Block.MarginProperty && (typeof(Paragraph).IsAssignableFrom(propertyOwnerType) || typeof(List).IsAssignableFrom(propertyOwnerType)))
||
(property == Block.PaddingProperty) && typeof(List).IsAssignableFrom(propertyOwnerType))
{
Thickness thickness = (Thickness)propertyValue;
if (Paragraph.IsMarginAuto(thickness))
{
continue;
}
}
// Write the property as attribute string or add it to a list of complex properties.
WriteNoninheritableProperty(xmlWriter, property, propertyValue, propertyOwnerType, onlyAffected, complexProperties, context.ReadLocalValue(property));
}
}
// Writes a value of an individual non-inheritable property in form of attribute string.
// If the value cannot be serialized as a string, adds the property to a collection of complexProperties.
// To minimize the amount of xaml produced, the property is skipped if its value is equal to its default value
// for the given element type - the propertyOwnerType.
// The flag onlyAffected=false means that we want to output all properties independently on
// if they are equal to their default values or not.
private static void WriteNoninheritableProperty(XmlWriter xmlWriter, DependencyProperty property, object propertyValue, Type propertyOwnerType, bool onlyAffected, DependencyObject complexProperties, object localValue)
{
bool write = false;
if (propertyValue != null &&
propertyValue != DependencyProperty.UnsetValue)
{
if (!onlyAffected)
{
write = true;
}
else
{
PropertyMetadata metadata = property.GetMetadata(propertyOwnerType);
write = (metadata == null) || !(TextSchema.ValuesAreEqual(propertyValue, /*defaultValue*/metadata.DefaultValue) && localValue == DependencyProperty.UnsetValue);
}
}
if (write)
{
string stringValue = DPTypeDescriptorContext.GetStringValue(property, propertyValue);
if (stringValue != null)
{
stringValue = FilterNaNStringValueForDoublePropertyType(stringValue, property.PropertyType);
// For the property name in this case we safe to use simple name only;
// as noninheritable property would never require TypeName.PropertyName notation
// for attribute syntax.
xmlWriter.WriteAttributeString(property.Name, stringValue);
}
else
{
complexProperties.SetValue(property, propertyValue);
}
}
}
// Writes a collection of attributes representing properties with local values set on them.
// If the value cannot be serialized as a string, adds the property to a collection of complexProperties.
private static void WriteLocallySetProperties(Type elementTypeStandardized, ITextPointer context, XmlWriter xmlWriter, DependencyObject complexProperties)
{
TextPointer textPointer = context as TextPointer;
if (textPointer == null)
{
// We can't have custom properties if we're not a TextPointer
return;
}
LocalValueEnumerator locallySetProperties = context.GetLocalValueEnumerator();
DependencyProperty[] inheritableProperties = TextSchema.GetInheritableProperties(elementTypeStandardized);
DependencyProperty[] nonInheritableProperties = TextSchema.GetNoninheritableProperties(elementTypeStandardized);
while (locallySetProperties.MoveNext())
{
DependencyProperty locallySetProperty = (DependencyProperty)locallySetProperties.Current.Property;
// Don't serialize read-only properties, or any properties registered or owned by a
// a class in the framework (we only want to serialize custom properties), to be
// consistent with our behavior for non-custom inlines.
// Does the IsKnownType call make the IsPropertyKnown call unnecessary?
if (!locallySetProperty.ReadOnly &&
!IsPropertyKnown(locallySetProperty, inheritableProperties, nonInheritableProperties) &&
!TextSchema.IsKnownType(locallySetProperty.OwnerType))
{
object propertyValue = context.ReadLocalValue(locallySetProperty);
string stringValue = DPTypeDescriptorContext.GetStringValue(locallySetProperty, propertyValue);
if (stringValue != null)
{
stringValue = FilterNaNStringValueForDoublePropertyType(stringValue, locallySetProperty.PropertyType);
string propertyName = GetPropertyNameForElement(locallySetProperty, elementTypeStandardized, /*forceComplexName:*/false);
xmlWriter.WriteAttributeString(propertyName, stringValue);
}
else
{
complexProperties.SetValue(locallySetProperty, propertyValue);
}
}
}
// *** WE NEED TO BETTER UNDERSTAND THE IMPLICATIONS OF SERIALIZING NON-DP CLR PROPERTIES, SO THE REST OF
// *** THIS METHOD IS DISABLED UNTIL WE DECIDE THE BEST WAY TO HANDLE THEM.
// *** CLRTypeDescriptorContext is essentially the same as DPTypeDescriptorContext.
#if false
// Check all CLR properties
// Note that this is partially redundant. TypeDescriptor.GetProperties, when called on a
// DependencyObject, will return all properties that are set-- including all those already
// serialized as Inheritable, NonInheritable, or LocallySet properties. A potential
// optimization, therefore, is to remove those serialization methods and simply use this one
// for everything when we've opted into custom element serialization.
PropertyDescriptorCollection descriptorCollection = TypeDescriptor.GetProperties(textPointer.Parent);
IEnumerator descriptors = descriptorCollection.GetEnumerator();
while (descriptors.MoveNext())
{
PropertyDescriptor current = (PropertyDescriptor)descriptors.Current;
// ShouldSerializeValue() will return true for readonly properties that have explicitly
// been told to serialize, such as Span.Inlines. If we serialize a read-only property,
// however, the parser will throw an exception when we try to deserialize. So we
// explicitly skip all read-only properties, and all DPs.
if (!current.ShouldSerializeValue(textPointer.Parent) || current.IsReadOnly || current is MS.Internal.ComponentModel.DependencyObjectPropertyDescriptor)
{
continue;
}
// Serialize the property
object propertyValue = current.GetValue(textPointer.Parent);
if (propertyValue != null)
{
string stringValue = CLRTypeDescriptorContext.GetStringValue(current, propertyValue);
if (stringValue != null)
{
stringValue = FilterNaNStringValueForDoublePropertyType(stringValue, current.PropertyType);
xmlWriter.WriteAttributeString(current.Name, stringValue);
}
else
{
// Need to Support complex properties
}
}
}
#endif
}
private static bool IsPropertyKnown(DependencyProperty propertyToTest, DependencyProperty[] inheritableProperties, DependencyProperty[] nonInheritableProperties)
{
for (int i = 0; i < inheritableProperties.Length; i++)
{
DependencyProperty property = inheritableProperties[i];
if (property == propertyToTest)
{
return true;
}
}
for (int i = 0; i < nonInheritableProperties.Length; i++)
{
DependencyProperty property = nonInheritableProperties[i];
if (property == propertyToTest)
{
return true;
}
}
return false;
}
/// <summary>
/// Writes complex properties in form of child elements with compound names
/// </summary>
private static void WriteComplexProperties(XmlWriter xmlWriter, DependencyObject complexProperties, Type elementType)
{
LocalValueEnumerator properties = complexProperties.GetLocalValueEnumerator();
properties.Reset();
while (properties.MoveNext())
{
LocalValueEntry propertyEntry = properties.Current;
// Build an appropriate name for the complex property
string complexPropertyName = GetPropertyNameForElement(propertyEntry.Property, elementType, /*forceComplexName:*/true);
// Write the start element for the complex property.
xmlWriter.WriteStartElement(complexPropertyName);
// Serialize the complex property value from SaveAsXml().
string complexPropertyXml = System.Windows.Markup.XamlWriter.Save(propertyEntry.Value);
// Write the serialized complext property value as Xml.
xmlWriter.WriteRaw(complexPropertyXml);
// Close the element for the complex property
xmlWriter.WriteEndElement();
}
}
// Creates a name for the property which is consistent with xaml parser logic
// When forceComplexName=true produces the TypeName.PropertyName notation unconditionally,
// otherwise such complex name is produced only when the TypeName is different from elementType.Name.
private static string GetPropertyNameForElement(DependencyProperty property, Type elementType, bool forceComplexName)
{
string propertyName;
if (DependencyProperty.FromName(property.Name, elementType) == property)
{
// The elementType is an owner of this property, so we can use its name
if (forceComplexName)
{
propertyName = $"{elementType.Name}.{property.Name}";
}
else
{
propertyName = property.Name;
}
}
else
{
// The elementType does not own this property, so we use the property's registered owner type name.
propertyName = $"{property.OwnerType.Name}.{property.Name}";
}
return propertyName;
}
// Serializes an element assuming that it does not have any children. Used for TableColumn
// Need to unify this method with WriteEmbeddedObject. They both do the same job now.
private static void WriteXamlAtomicElement(DependencyObject element, XmlWriter xmlWriter, bool reduceElement)
{
Type elementTypeStandardized = TextSchema.GetStandardElementType(element.GetType(), reduceElement);
DependencyProperty[] elementProperties = TextSchema.GetNoninheritableProperties(elementTypeStandardized);
xmlWriter.WriteStartElement(elementTypeStandardized.Name);
for (int i = 0; i < elementProperties.Length; i++)
{
DependencyProperty property = elementProperties[i];
object propertyValue = element.ReadLocalValue(property);
if (propertyValue != null && propertyValue != DependencyProperty.UnsetValue)
{
System.ComponentModel.TypeConverter typeConverter = System.ComponentModel.TypeDescriptor.GetConverter(property.PropertyType);
Invariant.Assert(typeConverter != null, "typeConverter==null: is not expected for atomic elements");
Invariant.Assert(typeConverter.CanConvertTo(typeof(string)), "type is expected to be convertable into string type");
string stringValue = (string)typeConverter.ConvertTo(/*ITypeDescriptorContext:*/null, CultureInfo.InvariantCulture, propertyValue, typeof(string));
Invariant.Assert(stringValue != null, "expecting non-null stringValue");
xmlWriter.WriteAttributeString(property.Name, stringValue);
}
}
xmlWriter.WriteEndElement();
}
/// <summary>
/// Writes embeded object tag.
/// </summary>
/// <param name="embeddedObject">
/// </param>
/// <param name="xmlWriter">
/// XmlWriter to output element opening tag.
/// </param>
/// <param name="wpfPayload">
/// </param>
private static void WriteEmbeddedObject(object embeddedObject, XmlWriter xmlWriter, WpfPayload wpfPayload)
{
if (wpfPayload != null && embeddedObject is Image)
{
// Writing in WPF mode: need to create an image with a Source referring into a package
Image image = (Image)embeddedObject;
if (image.Source != null && !string.IsNullOrEmpty(image.Source.ToString()))
{
// Add the image to the Image collection in the package
// and define the reference to image into the package
string imageSource = wpfPayload.AddImage(image);
if (imageSource != null)
{
Type elementTypeStandardized = typeof(Image);
// Write opening tag for the element
xmlWriter.WriteStartElement(elementTypeStandardized.Name);
// Write all properties except for Source
DependencyProperty[] imageProperties = TextSchema.ImageProperties;
DependencyObject complexProperties = new DependencyObject();
for (int i = 0; i < imageProperties.Length; i++)
{
DependencyProperty property = imageProperties[i];
if (property != Image.SourceProperty)
{
object value = image.GetValue(property);
// Write the property as attribute string or add it to a list of complex properties.
WriteNoninheritableProperty(xmlWriter, property, value, elementTypeStandardized, /*onlyAffected:*/true, complexProperties, image.ReadLocalValue(property));
}
}
// Write Source property - as a local reference into the package container
// Write Source property as the complex property to specify the BitmapImage
// cache option as "OnLoad" instead of the default "OnDeman". Otherwise,
// we couldn't load the image by disposing WpfPayload package.
xmlWriter.WriteStartElement($"{typeof(Image).Name}.{Image.SourceProperty.Name}");
xmlWriter.WriteStartElement(typeof(System.Windows.Media.Imaging.BitmapImage).Name);
xmlWriter.WriteAttributeString(System.Windows.Media.Imaging.BitmapImage.UriSourceProperty.Name, imageSource);
xmlWriter.WriteAttributeString(System.Windows.Media.Imaging.BitmapImage.CacheOptionProperty.Name, "OnLoad");
xmlWriter.WriteEndElement();
xmlWriter.WriteEndElement();
// Write remaining complex properties
WriteComplexProperties(xmlWriter, complexProperties, elementTypeStandardized);
// Close the element
xmlWriter.WriteEndElement();
}
}
}
else
{
// In non-package mode we ignore all UIElements.
// Output a space replacing this embedded element.
// Note that in this mode (DataFormats.Xaml) InlineUIContainer was
// replaced by Run and BlockUIContainer - by Paragraph,
// so the space output here will be significant.
xmlWriter.WriteString(" ");
}
}
// .............................................................
//
// Pasting
//
// .............................................................
// Handles a special case for pasting a single embedded element -
// needs to choose between BlockUIContainer and InlineUIContainer.
private static bool PasteSingleEmbeddedElement(TextRange range, TextElement fragment)
{
if (fragment.ContentStart.GetOffsetToPosition(fragment.ContentEnd) == 3)
{
TextElement uiContainer = fragment.ContentStart.GetAdjacentElement(LogicalDirection.Forward) as TextElement;
FrameworkElement embeddedElement = null;
if (uiContainer is BlockUIContainer)
{
embeddedElement = ((BlockUIContainer)uiContainer).Child as FrameworkElement;
if (embeddedElement != null)
{
((BlockUIContainer)uiContainer).Child = null;
}
}
else if (uiContainer is InlineUIContainer)
{
embeddedElement = ((InlineUIContainer)uiContainer).Child as FrameworkElement;
if (embeddedElement != null)
{
((InlineUIContainer)uiContainer).Child = null;
}
}
if (embeddedElement != null)
{
range.InsertEmbeddedUIElement(embeddedElement);
return true;
}
}
return false;
}
private static void PasteTextFragment(TextElement fragment, TextRange range)
{
Invariant.Assert(range.IsEmpty, "range must be empty at this point - emptied by a caller");
Invariant.Assert(fragment is Section || fragment is Span, "The wrapper element must be a Section or Span");
// Define insertion position.
TextPointer insertionPosition = TextRangeEditTables.EnsureInsertionPosition(range.End);
// Check if our insertion position has a non-splittable Inline ancestor such as Hyperlink element.
// Since we cannot split such Inline, we must switch to Text mode for pasting.
// Note that this also has the side effect of converting paragraph breaks to space characters.
if (insertionPosition.HasNonMergeableInlineAncestor)
{
PasteNonMergeableTextFragment(fragment, range);
}
else
{
PasteMergeableTextFragment(fragment, range, insertionPosition);
}
}
// Helper for PasteTextFragment
private static void PasteNonMergeableTextFragment(TextElement fragment, TextRange range)
{
// We cannot split Hyperlink or other non-splittable inline ancestor.
// Paste text content of fragment in such case.
// Get text content to be pasted.
string fragmentText = TextRangeBase.GetTextInternal(fragment.ElementStart, fragment.ElementEnd);
// Paste text into our empty target range.
// Replace this with SetTextInternal when it is implemented.
range.Text = fragmentText;
// Select pasted content
range.Select(range.Start, range.End);
}
// Helper for PasteTextFragment
private static void PasteMergeableTextFragment(TextElement fragment, TextRange range, TextPointer insertionPosition)
{
TextPointer fragmentStart;
TextPointer fragmentEnd;
if (fragment is Span)
{
// Split structure at insertion point in target
insertionPosition = TextRangeEdit.SplitFormattingElements(insertionPosition, /*keepEmptyFormatting:*/false);
Invariant.Assert(insertionPosition.Parent is Paragraph, "insertionPosition must be in a scope of a Paragraph after splitting formatting elements");
// Move the whole Span into the insertion point
fragment.RepositionWithContent(insertionPosition);
// Store edge positions of inserted content
fragmentStart = fragment.ElementStart;
fragmentEnd = fragment.ElementEnd;
// Remove wrapper from a tree
fragment.Reposition(null, null);
ValidateMergingPositions(typeof(Inline), fragmentStart, fragmentEnd);
// Transfer inheritable contextual properties
ApplyContextualProperties(fragmentStart, fragmentEnd, fragment);
}
else
{
// Correct leading nested List elements in the fragment
CorrectLeadingNestedLists((Section)fragment);
// Split a paragraph at insertion position
bool needFirstParagraphMerging = SplitParagraphForPasting(ref insertionPosition);
// Move the whole Section into the insertion point
fragment.RepositionWithContent(insertionPosition);
// Store edge positions of inserted content
fragmentStart = fragment.ElementStart;
fragmentEnd = fragment.ElementEnd.GetPositionAtOffset(0, LogicalDirection.Forward); // need forward orientation to stick with the following content during merge at fragmentStart position
// And unwrap the root Section
fragment.Reposition(null, null);
ValidateMergingPositions(typeof(Block), fragmentStart, fragmentEnd);
// Transfer inheritable contextual properties
ApplyContextualProperties(fragmentStart, fragmentEnd, fragment);
// Merge paragraphs on fragment boundaries
if (needFirstParagraphMerging)
{
MergeParagraphsAtPosition(fragmentStart, /*mergingOnFragmentStart:*/true);
}
// Get an indication that we need to merge last paragraph
if (!((Section)fragment).HasTrailingParagraphBreakOnPaste)
{
MergeParagraphsAtPosition(fragmentEnd, /*mergingOnFragmentStart:*/false);
}
}
// Whole-Document Heuristic: When initial content was empty we need to transfer flowDocument's properties: PageSize, FlowDirection, etc.
// For paragraph pasting move range end to the following paragraph, because
// it must include an ending paragraph break (in case of no-merging)
if (fragment is Section && ((Section)fragment).HasTrailingParagraphBreakOnPaste)
{
fragmentEnd = fragmentEnd.GetInsertionPosition(LogicalDirection.Forward);
}
// Select pasted content
range.Select(fragmentStart, fragmentEnd);
}
// Removes nested ListItems in the beginning of a fragment
// to avoid multiple bulleting.
// Better to avoid producing such nested ListItems on copy.
private static void CorrectLeadingNestedLists(Section fragment)
{
List list = fragment.Blocks.FirstBlock as List;
while (list != null)
{
ListItem listItem = list.ListItems.FirstListItem;
if (listItem == null)
{
return;
}
if (listItem.NextListItem != null)
{
return;
}
List nestedList = listItem.Blocks.FirstBlock as List;
if (nestedList == null)
{
return;
}
// So we have nested list in the very beginning of the outer single-item list:
// remove that outer list
listItem.Reposition(null, null);
list.Reposition(null, null);
list = nestedList;
}
}
// Decides whether we need to split a paragraph before pasting a fragment or not.
// Splits the paragraph if needed, or simply moves the insertionPosition before its start.
// Returns true if splitting happened and consequently merging is required after pasting.
private static bool SplitParagraphForPasting(ref TextPointer insertionPosition)
{
bool needFirstParagraphMerging = true; // we need splitting unless the position os at the bery beginniong of a paragraph
// When the insertion position is at the beginning of a paragraph we can avoid
// splitting and then merging paragraphs at fragment start position.
// This is not a pref consideration. We do not want an empty paragraph
// would kill a formatting of a first pasted paragraphs (say, ListItem of a pasted List).
TextPointer positionBeforeParagraph = insertionPosition;
// Skip formatting tags
while (positionBeforeParagraph.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart &&
TextSchema.IsFormattingType(positionBeforeParagraph.Parent.GetType()))
{
positionBeforeParagraph = positionBeforeParagraph.GetNextContextPosition(LogicalDirection.Backward);
}
while (positionBeforeParagraph.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart &&
TextSchema.AllowsParagraphMerging(positionBeforeParagraph.Parent.GetType()))
{
needFirstParagraphMerging = false;
positionBeforeParagraph = positionBeforeParagraph.GetNextContextPosition(LogicalDirection.Backward);
}
if (!needFirstParagraphMerging)
{
// Insertion position was in the beginning of a paragraph.
// No need in splitting/merging at fragment start
insertionPosition = positionBeforeParagraph;
}
else
{
// split paragraph to create an insertion positionn at block level
insertionPosition = TextRangeEdit.InsertParagraphBreak(insertionPosition, /*moveIntoSecondParagraph:*/false);
}
// When insertionPosition is inside a ListItem, then InsertParagraphBreak will
// split not only a parent Paragraph but also a ListItem and return a position
// between ListItems. This position is not good for inserting Block elements,
// so we also need to split parent List element.
// In a case when insertionPosition was at the beginning of a paragraph,
// we still can end up being between ListItems, so again need to split a parent List.
if (insertionPosition.Parent is List)
{
insertionPosition = TextRangeEdit.SplitElement(insertionPosition);
}
return needFirstParagraphMerging;
}
// Merges two paragraphs preceding and following the given position
private static void MergeParagraphsAtPosition(TextPointer position, bool mergingOnFragmentStart)
{
TextPointer navigator = position;
while (navigator != null && !(navigator.Parent is Paragraph))
{
if (navigator.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementEnd)
{
navigator = navigator.GetNextContextPosition(LogicalDirection.Backward);
}
else
{
navigator = null;
}
}
if (navigator != null)
{
Invariant.Assert(navigator.Parent is Paragraph, "We suppose have a first paragraph found");
Paragraph firstParagraph = (Paragraph)navigator.Parent;
navigator = position;
while (navigator != null && !(navigator.Parent is Paragraph))
{
if (navigator.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart)
{
navigator = navigator.GetNextContextPosition(LogicalDirection.Forward);
}
else
{
navigator = null;
}
}
if (navigator != null)
{
Invariant.Assert(navigator.Parent is Paragraph, "We suppose a second paragraph found");
Paragraph secondParagraph = (Paragraph)navigator.Parent;
if (TextRangeEditLists.ParagraphsAreMergeable(firstParagraph, secondParagraph))
{
TextRangeEditLists.MergeParagraphs(firstParagraph, secondParagraph);
}
else if (mergingOnFragmentStart && firstParagraph.TextRange.IsEmpty)
{
firstParagraph.RepositionWithContent(null);
}
else if (!mergingOnFragmentStart && secondParagraph.TextRange.IsEmpty)
{
secondParagraph.RepositionWithContent(null);
}
}
}
}
// Validates that the sibling element at this position belong to expected itemType (Inline, Block, ListItem)
private static void ValidateMergingPositions(Type itemType, TextPointer start, TextPointer end)
{
if (start.CompareTo(end) < 0)
{
// Verify inner part
TextPointerContext forwardFromStart = start.GetPointerContext(LogicalDirection.Forward);
TextPointerContext backwardFromEnd = end.GetPointerContext(LogicalDirection.Backward);
Invariant.Assert(forwardFromStart == TextPointerContext.ElementStart, "Expecting first opening tag of pasted fragment");
Invariant.Assert(backwardFromEnd == TextPointerContext.ElementEnd, "Expecting last closing tag of pasted fragment");
Invariant.Assert(itemType.IsAssignableFrom(start.GetAdjacentElement(LogicalDirection.Forward).GetType()),
$"The first pasted fragment item is expected to be a {itemType.Name}");
Invariant.Assert(itemType.IsAssignableFrom(end.GetAdjacentElement(LogicalDirection.Backward).GetType()),
$"The last pasted fragment item is expected to be a {itemType.Name}");
// Veryfy outer part
TextPointerContext backwardFromStart = start.GetPointerContext(LogicalDirection.Backward);
TextPointerContext forwardFromEnd = end.GetPointerContext(LogicalDirection.Forward);
Invariant.Assert(backwardFromStart == TextPointerContext.ElementStart || backwardFromStart == TextPointerContext.ElementEnd || backwardFromStart == TextPointerContext.None, "Bad context preceding a pasted fragment");
Invariant.Assert(!(backwardFromStart == TextPointerContext.ElementEnd) || itemType.IsAssignableFrom(start.GetAdjacentElement(LogicalDirection.Backward).GetType()),
$"An element preceding a pasted fragment is expected to be a {itemType.Name}");
Invariant.Assert(forwardFromEnd == TextPointerContext.ElementStart || forwardFromEnd == TextPointerContext.ElementEnd || forwardFromEnd == TextPointerContext.None, "Bad context following a pasted fragment");
Invariant.Assert(!(forwardFromEnd == TextPointerContext.ElementStart) || itemType.IsAssignableFrom(end.GetAdjacentElement(LogicalDirection.Forward).GetType()),
$"An element following a pasted fragment is expected to be a {itemType.Name}");
}
}
// Helper function used to set default value for an indicator requesting to merge last paragraph.
private static void AdjustFragmentForTargetRange(TextElement fragment, TextRange range)
{
if (fragment is Section && ((Section)fragment).HasTrailingParagraphBreakOnPaste)
{
// Explicit indicator is missing, we need to set it by default.
// In a case of TextRange.Xml property assignment we assume that
// user expects to insert as many paragraphs new paragraphs as her pasted xaml contains.
// The expection must be done to the case when the target range is
// extended beyond the last paragraph - then we must merge last paragraph
// to avoid extra paragraph creation at the end (one additional paragraph
// will be created in this case by Pasting code before pasting).
// The other case for exception is when target TextContainer is empty -
// in this case we as well want to merge last paragraph with the following
// one (which will be created as part of paragraph enforcement in pasting operation).
// The both desired conditions - IsAfterLastParagraph and "in empty container"
// can be identified by the following simple test - range.End is not at end-of-doc.
((Section)fragment).HasTrailingParagraphBreakOnPaste = range.End.GetPointerContext(LogicalDirection.Forward) != TextPointerContext.None;
}
}
// Applies a whole property bag to a range from start to end to simulate inheritance of this property from source conntext
private static void ApplyContextualProperties(TextPointer start, TextPointer end, TextElement propertyBag)
{
Invariant.Assert(propertyBag.IsEmpty && propertyBag.Parent == null, "propertyBag is supposed to be an empty element outside any tree");
LocalValueEnumerator contextualProperties = propertyBag.GetLocalValueEnumerator();
while (start.CompareTo(end) < 0 && contextualProperties.MoveNext())
{
// Note: we repeatedly check for IsEmpty because the selection
// may become empty as a result of normalization after formatting
// (thai character sequence).
LocalValueEntry propertyEntry = contextualProperties.Current;
DependencyProperty property = propertyEntry.Property;
if (TextSchema.IsCharacterProperty(property) &&
TextSchema.IsParagraphProperty(property))
{
// In case a property is both an Inline and Paragraph property,
// propertyBag element type (section or span) decides how it should be applied.
if (TextSchema.IsBlock(propertyBag.GetType()))
{
ApplyContextualProperty(typeof(Block), start, end, property, propertyEntry.Value);
}
else
{
ApplyContextualProperty(typeof(Inline), start, end, property, propertyEntry.Value);
}
}
else if (TextSchema.IsCharacterProperty(property))
{
ApplyContextualProperty(typeof(Inline), start, end, property, propertyEntry.Value);
}
else if (TextSchema.IsParagraphProperty(property))
{
ApplyContextualProperty(typeof(Block), start, end, property, propertyEntry.Value);
}
}
// Merge formatting elements at end position
TextRangeEdit.MergeFormattingInlines(start);
TextRangeEdit.MergeFormattingInlines(end);
}
// Applies one property to a range from start to end to simulate inheritance of this property from source conntext
private static void ApplyContextualProperty(Type targetType, TextPointer start, TextPointer end, DependencyProperty property, object value)
{
if (TextSchema.ValuesAreEqual(start.Parent.GetValue(property), value))
{
return; // The property at insertion position is the same as it was in source context. Nothing to do.
}
// Advance start pointer to enter pasted fragment
start = start.GetNextContextPosition(LogicalDirection.Forward);
while (start != null && start.CompareTo(end) < 0)
{
TextPointerContext passedContext = start.GetPointerContext(LogicalDirection.Backward);
if (passedContext == TextPointerContext.ElementStart)
{
TextElement element = (TextElement)start.Parent;
// Check if this element affects the property in question
if (element.ReadLocalValue(property) != DependencyProperty.UnsetValue ||
!TextSchema.ValuesAreEqual(element.GetValue(property), element.Parent.GetValue(property)))
{
// The element affects this property, so we can skip it
start = element.ElementEnd;
}
else if (targetType.IsAssignableFrom(element.GetType()))
{
start = element.ElementEnd;
if (targetType == typeof(Block) && start.CompareTo(end) > 0)
{
// Contextual properties should not apply to the last paragraph
// when it is merged with the following content -
// to avoid affecting the folowing visible content formatting.
break;
}
// This is topmost-level inline element which inherits this property.
// Set the value explicitly
if (!TextSchema.ValuesAreEqual(value, element.GetValue(property)))
{
element.ClearValue(property);
if (!TextSchema.ValuesAreEqual(value, element.GetValue(property)))
{
element.SetValue(property, value);
}
TextRangeEdit.MergeFormattingInlines(element.ElementStart);
}
}
else
{
// Traverse down into a structured (non-innline) element
start = start.GetNextContextPosition(LogicalDirection.Forward);
}
}
else
{
// Traverse up from any element
Invariant.Assert(passedContext != TextPointerContext.None, "TextPointerContext.None is not expected");
start = start.GetNextContextPosition(LogicalDirection.Forward);
}
}
}
// Returns a navigator scoped in the common ancestor for the start and end positions
// The navigator is positioned in the beginning of the ancestor's content.
// Modifies the common ancestor for hyperlink serialization heuristic - in case when the range is positioned at hyperlink boundaries.
// Since we need to write a hyperlink wrapper in this case, navigator is positioned before hyperlink element start.
private static ITextPointer FindSerializationCommonAncestor(ITextRange range)
{
// Create navigators for tree traversing looking for commonAncestor
ITextPointer commonAncestor = range.Start.CreatePointer();
ITextPointer runningEnd = range.End.CreatePointer();
// Find nearest common ancestor
while (!commonAncestor.HasEqualScope(runningEnd))
{
// Run all way from end up the tree to check if start is ancestor
runningEnd.MoveToPosition(range.End);
while (typeof(TextElement).IsAssignableFrom(runningEnd.ParentType) && !runningEnd.HasEqualScope(commonAncestor))
{
runningEnd.MoveToElementEdge(ElementEdge.AfterEnd);
}
if (runningEnd.HasEqualScope(commonAncestor))
{
break;
}
// Move start one level up
commonAncestor.MoveToElementEdge(ElementEdge.BeforeStart);
}
while (!IsAcceptableAncestor(commonAncestor, range))
{
commonAncestor.MoveToElementEdge(ElementEdge.BeforeStart);
}
if (typeof(TextElement).IsAssignableFrom(commonAncestor.ParentType))
{
commonAncestor.MoveToElementEdge(ElementEdge.AfterStart);
// Check for special case, when range start and end are at Hyperlink boundaries.
// Need to expand commonAncestor to Hyperlink element start, so that Hyperlink wrapper is written.
ITextPointer hyperlinkStart = GetHyperlinkStart(range);
if (hyperlinkStart != null)
{
commonAncestor = hyperlinkStart;
}
}
else
{
commonAncestor.MoveToPosition(commonAncestor.TextContainer.Start);
}
return commonAncestor;
}
// Verify that a pointer is an acceptable ancestor. Some types can't be ancestors at all, while
// non-typographic-only elements are unacceptable if the range being serialized does not include the
// element's start and end (because we don't want to serialize properties on such an element).
private static bool IsAcceptableAncestor(ITextPointer commonAncestor, ITextRange range)
{
if (typeof(TableRow).IsAssignableFrom(commonAncestor.ParentType) ||
typeof(TableRowGroup).IsAssignableFrom(commonAncestor.ParentType) ||
typeof(Table).IsAssignableFrom(commonAncestor.ParentType) ||
typeof(BlockUIContainer).IsAssignableFrom(commonAncestor.ParentType) ||
typeof(List).IsAssignableFrom(commonAncestor.ParentType) ||
typeof(Inline).IsAssignableFrom(commonAncestor.ParentType) && TextSchema.HasTextDecorations(commonAncestor.GetValue(Inline.TextDecorationsProperty)))
{
return false;
}
// We don't want to use any formatting from within a non-typographic-only element unless the entire
// element is selected (in which case, the ancestor candidate will already be outside that element.
// If there is such an element ANYWHERE in the ancestry, the only acceptable
// ancestor is outside the outermost such element.
ITextPointer navigator = commonAncestor.CreatePointer();
while (typeof(TextElement).IsAssignableFrom(navigator.ParentType))
{
TextElementEditingBehaviorAttribute behaviorAttribute = (TextElementEditingBehaviorAttribute)Attribute.GetCustomAttribute(navigator.ParentType, typeof(TextElementEditingBehaviorAttribute));
if (behaviorAttribute != null && !behaviorAttribute.IsTypographicOnly)
{
return false;
}
navigator.MoveToElementEdge(ElementEdge.BeforeStart);
}
return true;
}
// Removes (inplace) any invalid surrogate chars from an array.
// Returns the new array length, text.Length minus any stripped
// chars.
//
// Unicode surrogates are 32 bit references to abstract chars.
// A valid surrogate pair consists of a 16 bit code point (the
// high surrogate) in the range u+d800 - u+dbff, followed by a
// second 16 bit code point (the low surrogate) in the range
// u+dc00 - u+dfff.
//
// An invalid surrogate is a high surrogate not followed by a
// low surrogate, or a low surrogate not preceeded by a high
// surrogate.
//
// Length specifies the number of characters in text to examine,
// characters past length are ignored (as if text.Length == length).
//
// Also removes nul characters
private static int StripInvalidSurrogateChars(char[] text, int length)
{
int count;
Invariant.Assert(text.Length >= length, "Asserting that text.Length >= length");
int i;
for (i = 0; i < length; i++)
{
char testChar = text[i];
if (Char.IsHighSurrogate(testChar) || Char.IsLowSurrogate(testChar) || IsBadCode(testChar))
{
break;
}
}
if (i == length)
{
// No surrogates, early out.
count = length;
}
else
{
count = i;
for (; i < length; i++)
{
if (Char.IsHighSurrogate(text[i]))
{
if (i + 1 < length && Char.IsLowSurrogate(text[i + 1]))
{
// Valid surrogate encountered.
text[count] = text[i];
text[count + 1] = text[i + 1];
count += 2;
i++; // Skip over low surrogate.
}
else
{
// Bogus high surrogte encountered -- remove it.
// Simply don't update destinationIndex or count.
}
}
else if (Char.IsLowSurrogate(text[i]))
{
// Bogus low surrogate encountered -- remove it.
// Simply don't update destinationIndex or count.
}
else if (IsBadCode(text[i]))
{
// nul character enountered - remove it.
// Simply don't update destinationIndex or count.
// What about other control codes? Shouldn't they be also stripped out?
}
else
{
// Non-surrogate encountered.
text[count] = text[i];
count += 1;
}
}
}
return count;
}
private static bool IsBadCode(char code)
{
return (code < ' ' && code != '\x0009' && code != '\x000A' && code != '\x000D');
}
/// <summary>
/// Return true if rangeEnd is not at the end of an element.
///
/// textReader must already be at the start of the element.
/// </summary>
private static bool IsPartialNonTypographic(ITextPointer textReader, ITextPointer rangeEnd)
{
bool isPartial = false;
Invariant.Assert(textReader.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart);
ITextPointer elementNavigation = textReader.CreatePointer();
ITextPointer elementEnd = textReader.CreatePointer();
elementEnd.MoveToNextContextPosition(LogicalDirection.Forward);
// Find the end position
elementEnd.MoveToElementEdge(ElementEdge.AfterEnd);
if (elementEnd.CompareTo(rangeEnd) > 0)
{
isPartial = true;
}
return isPartial;
}
/// <summary>
/// Return true if Hyperlink range is invalid.
/// Hyperlink is invalid if it include a UiElement except Image or the range end position
/// is stated before the end position of hyperlink.
/// This must be called before Hyperlink start element position.
/// </summary>
private static bool IsHyperlinkInvalid(ITextPointer textReader, ITextPointer rangeEnd)
{
// TextRead must be on the position before the element start position of Hyperlink
Invariant.Assert(textReader.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.ElementStart);
Invariant.Assert(typeof(Hyperlink).IsAssignableFrom(textReader.GetElementType(LogicalDirection.Forward)));
bool hyperlinkInvalid = false;
// Get the forward adjacent element and cast Hyperlink hardly since it must be Hyperlink
Hyperlink hyperlink = (Hyperlink)textReader.GetAdjacentElement(LogicalDirection.Forward);
ITextPointer hyperlinkNavigation = textReader.CreatePointer();
ITextPointer hyperlinkEnd = textReader.CreatePointer();
hyperlinkEnd.MoveToNextContextPosition(LogicalDirection.Forward);
// Find the hyperlink end position
hyperlinkEnd.MoveToElementEdge(ElementEdge.AfterEnd);
// Hyperlink end position is stated after the range end position.
if (hyperlinkEnd.CompareTo(rangeEnd) > 0)
{
hyperlinkInvalid = true;
}
else
{
// Check whether the hyperlink having a UiElement except Image until hyperlink end position
while (hyperlinkNavigation.CompareTo(hyperlinkEnd) < 0)
{
InlineUIContainer inlineUIContainer = hyperlinkNavigation.GetAdjacentElement(LogicalDirection.Forward) as InlineUIContainer;
if (inlineUIContainer != null && !(inlineUIContainer.Child is Image))
{
hyperlinkInvalid = true;
break;
}
hyperlinkNavigation.MoveToNextContextPosition(LogicalDirection.Forward);
}
}
return hyperlinkInvalid;
}
// Returns a position before hyperlink element start if passed range start and end are at hyperlink boundaries. Otherwise, null.
private static ITextPointer GetHyperlinkStart(ITextRange range)
{
ITextPointer hyperlinkStart = null;
if (TextPointerBase.IsAtNonMergeableInlineStart(range.Start) && TextPointerBase.IsAtNonMergeableInlineEnd(range.End))
{
// Find a position at hyperlink start.
hyperlinkStart = range.Start.CreatePointer(LogicalDirection.Forward);
while (hyperlinkStart.GetPointerContext(LogicalDirection.Backward) == TextPointerContext.ElementStart &&
!typeof(Hyperlink).IsAssignableFrom(hyperlinkStart.ParentType))
{
hyperlinkStart.MoveToElementEdge(ElementEdge.BeforeStart);
}
hyperlinkStart.MoveToElementEdge(ElementEdge.BeforeStart);
hyperlinkStart.Freeze();
}
return hyperlinkStart;
}
private static string FilterNaNStringValueForDoublePropertyType(string stringValue, Type propertyType)
{
if (propertyType == typeof(double) && string.Equals(stringValue, "NaN", StringComparison.OrdinalIgnoreCase))
{
return "Auto"; // convert NaN to Auto, to keep parser happy
}
return stringValue;
}
#endregion Private Methods
// -------------------------------------------------------------
//
// Private Constants
//
// -------------------------------------------------------------
#region Private Constants
// A structural depth of a empty FlowDocument fragment
private const int EmptyDocumentDepth = 1;
#endregion Private Constants
}
}
|