Skip to content
Original file line number Diff line number Diff line change
@@ -1,273 +1,159 @@
// 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 System.ComponentModel;
using System.ComponentModel.Design.Serialization;
using System.ComponentModel;
using System.Globalization;
using System.Reflection;

namespace System.Windows
{
/// <summary>
/// TypeConverter for TextDecorationCollection
/// </summary>
/// Provides a type converter to convert from <see cref="string"/> to <see cref="TextDecorationCollection"/> only.
/// </summary>
public sealed class TextDecorationCollectionConverter : TypeConverter
{
/// <summary>
/// CanConvertTo method
/// Returns whether this converter can convert the object to the specified
/// <paramref name="destinationType"/>, using the specified <paramref name="context"/>.
/// </summary>
/// <param name="context"> ITypeDescriptorContext </param>
/// <param name="destinationType"> Type to convert to </param>
/// <returns> false will always be returned because TextDecorations cannot be converted to any other type. </returns>
/// <param name="context">Context information used for conversion.</param>
/// <param name="destinationType">Type being evaluated for conversion.</param>
/// <returns>
/// <see langword="false"/> will always be returned because <see cref="TextDecorations"/> cannot be converted to any other type.
/// </returns>
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
{
if (destinationType == typeof(InstanceDescriptor))
{
return true;
}

// return false for any other target type. Don't call base.CanConvertTo() because it would be confusing
// in some cases. For example, for destination typeof(String), base convertor just converts the TDC to the
// string full name of the type.
return false;
// Return false for any other target type. Don't call base.CanConvertTo() because it would be confusing
// in some cases. For example, for destination typeof(string), base TypeConverter just converts the
// ITypeDescriptorContext to the full name string of the given type.
return destinationType == typeof(InstanceDescriptor);
}

/// <summary>
/// CanConvertFrom
/// Returns whether this class can convert specific <see cref="Type"/> into <see cref="TextDecorationCollection"/>.
/// </summary>
/// <param name="context"> ITypeDescriptorContext </param>
/// <param name="sourceType">Type to convert to </param>
/// <returns> true if it can convert from sourceType to TextDecorations, false otherwise </returns>
/// <param name="sourceType">Type being evaluated for conversion.</param>
/// <returns>
/// <see langword="true"/> if <paramref name="sourceType"/> is <see langword="string"/>, otherwise <see langword="false"/>.
/// </returns>
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
if (sourceType == typeof(string))
{
return true;
}

return false;
return sourceType == typeof(string);
}

/// <summary>
/// ConvertFrom
/// Converts <paramref name="input"/> of <see langword="string"/> type to its <see cref="TextDecorationCollection"/> representation.
/// </summary>
/// <param name="context"> ITypeDescriptorContext </param>
/// <param name="culture"> CultureInfo </param>
/// <param name="input"> The input object to be converted to TextDecorations </param>
/// <returns> the converted value of the input object </returns>
/// <param name="context">Context information used for conversion, ignored currently.</param>
/// <param name="culture">The culture specifier to use, ignored currently.</param>
/// <param name="input">The string to convert from.</param>
/// <returns>A <see cref="TextDecorationCollection"/> representing the <see langword="string"/> specified by <paramref name="input"/>.</returns>
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object input)
{
if (input == null)
{
if (input is null)
throw GetConvertFromException(input);
}

string value = input as string;

if (null == value)
{
if (input is not string value)
throw new ArgumentException(SR.Format(SR.General_BadType, "ConvertFrom"), nameof(input));
}

return ConvertFromString(value);

return ConvertFromString(value);
}

/// <summary>
/// ConvertFromString
/// Converts <paramref name="text"/> to its <see cref="TextDecorationCollection"/> representation.
/// </summary>
/// <param name="text"> The string to be converted into TextDecorationCollection object </param>
/// <returns> the converted value of the string flag </returns>
/// <param name="text">The string to be converted into TextDecorationCollection object.</param>
/// <returns>A <see cref="TextDecorationCollection"/> representing the <see cref="string"/> specified by <paramref name="text"/>.</returns>
/// <remarks>
/// The text parameter can be either string "None" or a combination of the predefined
/// TextDecoration names delimited by commas (,). One or more blanks spaces can precede
/// or follow each text decoration name or comma. There can't be duplicate TextDecoration names in the
/// string. The operation is case-insensitive.
/// The text parameter can be either be <see langword="null"/>; <see cref="string.Empty"/>; the string "None"
/// or a combination of the predefined <see cref="TextDecorations"/> names delimited by commas (,).
/// One or more blanks spaces can precede or follow each text decoration name or comma.
/// There can't be duplicate TextDecoration names in the string. The operation is case-insensitive.
/// </remarks>
public static new TextDecorationCollection ConvertFromString(string text)
{
if (text == null)
{
return null;
}
{
if (text is null)
return null;

// Flags indicating which pre-defined TextDecoration have been matched
Decorations matchedDecorations = Decorations.None;

// Sanitize the input
ReadOnlySpan<char> decorationsSpan = text.AsSpan().Trim();

TextDecorationCollection textDecorations = new TextDecorationCollection();
// Test for "None", which equals to empty collection and needs to be specified alone
if (decorationsSpan.IsEmpty || decorationsSpan.Equals("None", StringComparison.OrdinalIgnoreCase))
return new TextDecorationCollection();

// Flags indicating which Predefined textDecoration has alrady been added.
byte MatchedTextDecorationFlags = 0;

// Start from the 1st non-whitespace character
// Negative index means error is encountered
int index = AdvanceToNextNonWhiteSpace(text, 0);
while (index >= 0 && index < text.Length)
// Create new collection, save re-allocations
TextDecorationCollection textDecorations = new(1 + decorationsSpan.Count(','));
foreach (Range segment in decorationsSpan.Split(','))
{
if (Match(None, text, index))
ReadOnlySpan<char> decoration = decorationsSpan[segment].Trim();

if (decoration.Equals("Overline", StringComparison.OrdinalIgnoreCase) && !matchedDecorations.HasFlag(Decorations.OverlineMatch))
{
textDecorations.Add(TextDecorations.OverLine[0]);
matchedDecorations |= Decorations.OverlineMatch;
}
else if (decoration.Equals("Baseline", StringComparison.OrdinalIgnoreCase) && !matchedDecorations.HasFlag(Decorations.BaselineMatch))
{
// Matched "None" in the input
index = AdvanceToNextNonWhiteSpace(text, index + None.Length);
if (textDecorations.Count > 0 || index < text.Length)
{
// Error: "None" can only be specified by its own
index = -1;
}
textDecorations.Add(TextDecorations.Baseline[0]);
matchedDecorations |= Decorations.BaselineMatch;
}
else if (decoration.Equals("Underline", StringComparison.OrdinalIgnoreCase) && !matchedDecorations.HasFlag(Decorations.UnderlineMatch))
{
textDecorations.Add(TextDecorations.Underline[0]);
matchedDecorations |= Decorations.UnderlineMatch;
}
else if (decoration.Equals("Strikethrough", StringComparison.OrdinalIgnoreCase) && !matchedDecorations.HasFlag(Decorations.StrikethroughMatch))
{
textDecorations.Add(TextDecorations.Strikethrough[0]);
matchedDecorations |= Decorations.StrikethroughMatch;
}
else
{
// Match the input with one of the predefined text decoration names
int i;
for(i = 0;
i < TextDecorationNames.Length
&& !Match(TextDecorationNames[i], text, index);
i++
);

if (i < TextDecorationNames.Length)
{
// Found a match within the predefined names
if ((MatchedTextDecorationFlags & (1 << i)) > 0)
{
// Error: The matched value is duplicated.
index = -1;
}
else
{
// Valid match. Add to the collection and remember that this text decoration
// has been added
textDecorations.Add(PredefinedTextDecorations[i]);
MatchedTextDecorationFlags |= (byte)(1 << i);

// Advance to the start of next name
index = AdvanceToNextNameStart(text, index + TextDecorationNames[i].Length);
}
}
else
{
// Error: no match found in the predefined names
index = -1;
}
throw new ArgumentException(SR.Format(SR.InvalidTextDecorationCollectionString, text));
}
}

if (index < 0)
{
throw new ArgumentException(SR.Format(SR.InvalidTextDecorationCollectionString, text));
}

return textDecorations;
}
return textDecorations;
}

/// <summary>
/// ConvertTo
/// Converts a <paramref name="value"/> of <see cref="TextDecorationCollection"/> to the specified <paramref name="destinationType"/>.
/// </summary>
/// <param name="context"> ITypeDescriptorContext </param>
/// <param name="culture"> CultureInfo </param>
/// <param name="value"> the object to be converted to another type </param>
/// <param name="destinationType"> The destination type of the conversion </param>
/// <returns> null will always be returned because TextDecorations cannot be converted to any other type. </returns>
/// <param name="context">Context information used for conversion.</param>
/// <param name="culture">The culture specifier to use.</param>
/// <param name="value">Duration value to convert from.</param>
/// <param name="destinationType">Type being evaluated for conversion.</param>
/// <returns><see langword="null"/> will always be returned because <see cref="TextDecorations"/> cannot be converted to any other type.</returns>
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
{
if (destinationType == typeof(InstanceDescriptor) && value is IEnumerable<TextDecoration>)
{
ConstructorInfo ci = typeof(TextDecorationCollection).GetConstructor(
new Type[]{typeof(IEnumerable<TextDecoration>)}
);

return new InstanceDescriptor(ci, new object[]{value});
}

// Pass unhandled cases to base class (which will throw exceptions for null value or destinationType.)
return base.ConvertTo(context, culture, value, destinationType);
}

//---------------------------------
// Private methods
//---------------------------------
/// <summary>
/// Match the input against a predefined pattern from certain index onwards
/// </summary>
private static bool Match(string pattern, string input, int index)
{
int i = 0;
for (;
i < pattern.Length
&& index + i < input.Length
&& pattern[i] == Char.ToUpperInvariant(input[index + i]);
i++) ;

return (i == pattern.Length);
}

/// <summary>
/// Advance to the start of next name
/// </summary>
private static int AdvanceToNextNameStart(string input, int index)
{
// Two names must be seperated by a comma and optionally spaces
int separator = AdvanceToNextNonWhiteSpace(input, index);
ConstructorInfo ci = typeof(TextDecorationCollection).GetConstructor(new Type[] { typeof(IEnumerable<TextDecoration>) });

int nextNameStart;
if (separator >= input.Length)
{
// reach the end
nextNameStart = input.Length;
}
else
{
if (input[separator] == Separator)
{
nextNameStart = AdvanceToNextNonWhiteSpace(input, separator + 1);
if (nextNameStart >= input.Length)
{
// Error: Separator is at the end of the input
nextNameStart = -1;
}
}
else
{
// Error: There is a non-whitespace, non-separator character following
// the matched value
nextNameStart = -1;
}
return new InstanceDescriptor(ci, new object[] { value });
}

return nextNameStart;
// Pass unhandled cases to base class (which will throw exceptions for null value or destinationType.)
return base.ConvertTo(context, culture, value, destinationType);
}

/// <summary>
/// Advance to the next non-whitespace character
/// Abstraction helper of matched decorations during conversion.
/// </summary>
private static int AdvanceToNextNonWhiteSpace(string input, int index)
[Flags]
private enum Decorations : byte
{
for (; index < input.Length && Char.IsWhiteSpace(input[index]); index++) ;
return (index > input.Length) ? input.Length : index;
None = 0,
OverlineMatch = 1 << 0,
BaselineMatch = 1 << 1,
UnderlineMatch = 1 << 2,
StrikethroughMatch = 1 << 3,
}

//---------------------------------
// Private members
//---------------------------------

//
// Predefined valid names for TextDecorations
// Names should be normalized to be upper case
//
private const string None = "NONE";
private const char Separator = ',';

private static readonly string[] TextDecorationNames = new string[] {
"OVERLINE",
"BASELINE",
"UNDERLINE",
"STRIKETHROUGH"
};

// Predefined TextDecorationCollection values. It should match
// the TextDecorationNames array
private static readonly TextDecorationCollection[] PredefinedTextDecorations =
new TextDecorationCollection[] {
TextDecorations.OverLine,
TextDecorations.Baseline,
TextDecorations.Underline,
TextDecorations.Strikethrough
};
}
}
}