|
1 | 1 | // Licensed to the .NET Foundation under one or more agreements. |
2 | 2 | // The .NET Foundation licenses this file to you under the MIT license. |
3 | 3 |
|
4 | | -using System.ComponentModel; |
5 | 4 | using System.ComponentModel.Design.Serialization; |
| 5 | +using System.ComponentModel; |
6 | 6 | using System.Globalization; |
7 | 7 | using System.Reflection; |
8 | 8 |
|
9 | 9 | namespace System.Windows |
10 | 10 | { |
11 | 11 | /// <summary> |
12 | | - /// TypeConverter for TextDecorationCollection |
13 | | - /// </summary> |
| 12 | + /// Provides a type converter to convert from <see cref="string"/> to <see cref="TextDecorationCollection"/> only. |
| 13 | + /// </summary> |
14 | 14 | public sealed class TextDecorationCollectionConverter : TypeConverter |
15 | 15 | { |
16 | 16 | /// <summary> |
17 | | - /// CanConvertTo method |
| 17 | + /// Returns whether this converter can convert the object to the specified |
| 18 | + /// <paramref name="destinationType"/>, using the specified <paramref name="context"/>. |
18 | 19 | /// </summary> |
19 | | - /// <param name="context"> ITypeDescriptorContext </param> |
20 | | - /// <param name="destinationType"> Type to convert to </param> |
21 | | - /// <returns> false will always be returned because TextDecorations cannot be converted to any other type. </returns> |
| 20 | + /// <param name="context">Context information used for conversion.</param> |
| 21 | + /// <param name="destinationType">Type being evaluated for conversion.</param> |
| 22 | + /// <returns> |
| 23 | + /// <see langword="false"/> will always be returned because <see cref="TextDecorations"/> cannot be converted to any other type. |
| 24 | + /// </returns> |
22 | 25 | public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) |
23 | 26 | { |
24 | | - if (destinationType == typeof(InstanceDescriptor)) |
25 | | - { |
26 | | - return true; |
27 | | - } |
28 | | - |
29 | | - // return false for any other target type. Don't call base.CanConvertTo() because it would be confusing |
30 | | - // in some cases. For example, for destination typeof(String), base convertor just converts the TDC to the |
31 | | - // string full name of the type. |
32 | | - return false; |
| 27 | + // Return false for any other target type. Don't call base.CanConvertTo() because it would be confusing |
| 28 | + // in some cases. For example, for destination typeof(string), base TypeConverter just converts the |
| 29 | + // ITypeDescriptorContext to the full name string of the given type. |
| 30 | + return destinationType == typeof(InstanceDescriptor); |
33 | 31 | } |
34 | 32 |
|
35 | 33 | /// <summary> |
36 | | - /// CanConvertFrom |
| 34 | + /// Returns whether this class can convert specific <see cref="Type"/> into <see cref="TextDecorationCollection"/>. |
37 | 35 | /// </summary> |
38 | 36 | /// <param name="context"> ITypeDescriptorContext </param> |
39 | | - /// <param name="sourceType">Type to convert to </param> |
40 | | - /// <returns> true if it can convert from sourceType to TextDecorations, false otherwise </returns> |
| 37 | + /// <param name="sourceType">Type being evaluated for conversion.</param> |
| 38 | + /// <returns> |
| 39 | + /// <see langword="true"/> if <paramref name="sourceType"/> is <see langword="string"/>, otherwise <see langword="false"/>. |
| 40 | + /// </returns> |
41 | 41 | public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
42 | 42 | { |
43 | | - if (sourceType == typeof(string)) |
44 | | - { |
45 | | - return true; |
46 | | - } |
47 | | - |
48 | | - return false; |
| 43 | + return sourceType == typeof(string); |
49 | 44 | } |
50 | 45 |
|
51 | 46 | /// <summary> |
52 | | - /// ConvertFrom |
| 47 | + /// Converts <paramref name="input"/> of <see langword="string"/> type to its <see cref="TextDecorationCollection"/> representation. |
53 | 48 | /// </summary> |
54 | | - /// <param name="context"> ITypeDescriptorContext </param> |
55 | | - /// <param name="culture"> CultureInfo </param> |
56 | | - /// <param name="input"> The input object to be converted to TextDecorations </param> |
57 | | - /// <returns> the converted value of the input object </returns> |
| 49 | + /// <param name="context">Context information used for conversion, ignored currently.</param> |
| 50 | + /// <param name="culture">The culture specifier to use, ignored currently.</param> |
| 51 | + /// <param name="input">The string to convert from.</param> |
| 52 | + /// <returns>A <see cref="TextDecorationCollection"/> representing the <see langword="string"/> specified by <paramref name="input"/>.</returns> |
58 | 53 | public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object input) |
59 | 54 | { |
60 | | - if (input == null) |
61 | | - { |
| 55 | + if (input is null) |
62 | 56 | throw GetConvertFromException(input); |
63 | | - } |
64 | 57 |
|
65 | | - string value = input as string; |
66 | | - |
67 | | - if (null == value) |
68 | | - { |
| 58 | + if (input is not string value) |
69 | 59 | throw new ArgumentException(SR.Format(SR.General_BadType, "ConvertFrom"), nameof(input)); |
70 | | - } |
71 | | - |
72 | | - return ConvertFromString(value); |
| 60 | + |
| 61 | + return ConvertFromString(value); |
73 | 62 | } |
74 | 63 |
|
75 | 64 | /// <summary> |
76 | | - /// ConvertFromString |
| 65 | + /// Converts <paramref name="text"/> to its <see cref="TextDecorationCollection"/> representation. |
77 | 66 | /// </summary> |
78 | | - /// <param name="text"> The string to be converted into TextDecorationCollection object </param> |
79 | | - /// <returns> the converted value of the string flag </returns> |
| 67 | + /// <param name="text">The string to be converted into TextDecorationCollection object.</param> |
| 68 | + /// <returns>A <see cref="TextDecorationCollection"/> representing the <see cref="string"/> specified by <paramref name="text"/>.</returns> |
80 | 69 | /// <remarks> |
81 | | - /// The text parameter can be either string "None" or a combination of the predefined |
82 | | - /// TextDecoration names delimited by commas (,). One or more blanks spaces can precede |
83 | | - /// or follow each text decoration name or comma. There can't be duplicate TextDecoration names in the |
84 | | - /// string. The operation is case-insensitive. |
| 70 | + /// The text parameter can be either be <see langword="null"/>; <see cref="string.Empty"/>; the string "None" |
| 71 | + /// or a combination of the predefined <see cref="TextDecorations"/> names delimited by commas (,). |
| 72 | + /// One or more blanks spaces can precede or follow each text decoration name or comma. |
| 73 | + /// There can't be duplicate TextDecoration names in the string. The operation is case-insensitive. |
85 | 74 | /// </remarks> |
86 | 75 | public static new TextDecorationCollection ConvertFromString(string text) |
87 | | - { |
88 | | - if (text == null) |
89 | | - { |
90 | | - return null; |
91 | | - } |
| 76 | + { |
| 77 | + if (text is null) |
| 78 | + return null; |
| 79 | + |
| 80 | + // Flags indicating which pre-defined TextDecoration have been matched |
| 81 | + Decorations matchedDecorations = Decorations.None; |
| 82 | + |
| 83 | + // Sanitize the input |
| 84 | + ReadOnlySpan<char> decorationsSpan = text.AsSpan().Trim(); |
92 | 85 |
|
93 | | - TextDecorationCollection textDecorations = new TextDecorationCollection(); |
| 86 | + // Test for "None", which equals to empty collection and needs to be specified alone |
| 87 | + if (decorationsSpan.IsEmpty || decorationsSpan.Equals("None", StringComparison.OrdinalIgnoreCase)) |
| 88 | + return new TextDecorationCollection(); |
94 | 89 |
|
95 | | - // Flags indicating which Predefined textDecoration has alrady been added. |
96 | | - byte MatchedTextDecorationFlags = 0; |
97 | | - |
98 | | - // Start from the 1st non-whitespace character |
99 | | - // Negative index means error is encountered |
100 | | - int index = AdvanceToNextNonWhiteSpace(text, 0); |
101 | | - while (index >= 0 && index < text.Length) |
| 90 | + // Create new collection, save re-allocations |
| 91 | + TextDecorationCollection textDecorations = new(1 + decorationsSpan.Count(',')); |
| 92 | + foreach (Range segment in decorationsSpan.Split(',')) |
102 | 93 | { |
103 | | - if (Match(None, text, index)) |
| 94 | + ReadOnlySpan<char> decoration = decorationsSpan[segment].Trim(); |
| 95 | + |
| 96 | + if (decoration.Equals("Overline", StringComparison.OrdinalIgnoreCase) && !matchedDecorations.HasFlag(Decorations.OverlineMatch)) |
| 97 | + { |
| 98 | + textDecorations.Add(TextDecorations.OverLine[0]); |
| 99 | + matchedDecorations |= Decorations.OverlineMatch; |
| 100 | + } |
| 101 | + else if (decoration.Equals("Baseline", StringComparison.OrdinalIgnoreCase) && !matchedDecorations.HasFlag(Decorations.BaselineMatch)) |
104 | 102 | { |
105 | | - // Matched "None" in the input |
106 | | - index = AdvanceToNextNonWhiteSpace(text, index + None.Length); |
107 | | - if (textDecorations.Count > 0 || index < text.Length) |
108 | | - { |
109 | | - // Error: "None" can only be specified by its own |
110 | | - index = -1; |
111 | | - } |
| 103 | + textDecorations.Add(TextDecorations.Baseline[0]); |
| 104 | + matchedDecorations |= Decorations.BaselineMatch; |
| 105 | + } |
| 106 | + else if (decoration.Equals("Underline", StringComparison.OrdinalIgnoreCase) && !matchedDecorations.HasFlag(Decorations.UnderlineMatch)) |
| 107 | + { |
| 108 | + textDecorations.Add(TextDecorations.Underline[0]); |
| 109 | + matchedDecorations |= Decorations.UnderlineMatch; |
| 110 | + } |
| 111 | + else if (decoration.Equals("Strikethrough", StringComparison.OrdinalIgnoreCase) && !matchedDecorations.HasFlag(Decorations.StrikethroughMatch)) |
| 112 | + { |
| 113 | + textDecorations.Add(TextDecorations.Strikethrough[0]); |
| 114 | + matchedDecorations |= Decorations.StrikethroughMatch; |
112 | 115 | } |
113 | 116 | else |
114 | 117 | { |
115 | | - // Match the input with one of the predefined text decoration names |
116 | | - int i; |
117 | | - for(i = 0; |
118 | | - i < TextDecorationNames.Length |
119 | | - && !Match(TextDecorationNames[i], text, index); |
120 | | - i++ |
121 | | - ); |
122 | | - |
123 | | - if (i < TextDecorationNames.Length) |
124 | | - { |
125 | | - // Found a match within the predefined names |
126 | | - if ((MatchedTextDecorationFlags & (1 << i)) > 0) |
127 | | - { |
128 | | - // Error: The matched value is duplicated. |
129 | | - index = -1; |
130 | | - } |
131 | | - else |
132 | | - { |
133 | | - // Valid match. Add to the collection and remember that this text decoration |
134 | | - // has been added |
135 | | - textDecorations.Add(PredefinedTextDecorations[i]); |
136 | | - MatchedTextDecorationFlags |= (byte)(1 << i); |
137 | | - |
138 | | - // Advance to the start of next name |
139 | | - index = AdvanceToNextNameStart(text, index + TextDecorationNames[i].Length); |
140 | | - } |
141 | | - } |
142 | | - else |
143 | | - { |
144 | | - // Error: no match found in the predefined names |
145 | | - index = -1; |
146 | | - } |
| 118 | + throw new ArgumentException(SR.Format(SR.InvalidTextDecorationCollectionString, text)); |
147 | 119 | } |
148 | 120 | } |
149 | 121 |
|
150 | | - if (index < 0) |
151 | | - { |
152 | | - throw new ArgumentException(SR.Format(SR.InvalidTextDecorationCollectionString, text)); |
153 | | - } |
154 | | - |
155 | | - return textDecorations; |
156 | | - } |
| 122 | + return textDecorations; |
| 123 | + } |
157 | 124 |
|
158 | 125 | /// <summary> |
159 | | - /// ConvertTo |
| 126 | + /// Converts a <paramref name="value"/> of <see cref="TextDecorationCollection"/> to the specified <paramref name="destinationType"/>. |
160 | 127 | /// </summary> |
161 | | - /// <param name="context"> ITypeDescriptorContext </param> |
162 | | - /// <param name="culture"> CultureInfo </param> |
163 | | - /// <param name="value"> the object to be converted to another type </param> |
164 | | - /// <param name="destinationType"> The destination type of the conversion </param> |
165 | | - /// <returns> null will always be returned because TextDecorations cannot be converted to any other type. </returns> |
| 128 | + /// <param name="context">Context information used for conversion.</param> |
| 129 | + /// <param name="culture">The culture specifier to use.</param> |
| 130 | + /// <param name="value">Duration value to convert from.</param> |
| 131 | + /// <param name="destinationType">Type being evaluated for conversion.</param> |
| 132 | + /// <returns><see langword="null"/> will always be returned because <see cref="TextDecorations"/> cannot be converted to any other type.</returns> |
166 | 133 | public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType) |
167 | 134 | { |
168 | 135 | if (destinationType == typeof(InstanceDescriptor) && value is IEnumerable<TextDecoration>) |
169 | 136 | { |
170 | | - ConstructorInfo ci = typeof(TextDecorationCollection).GetConstructor( |
171 | | - new Type[]{typeof(IEnumerable<TextDecoration>)} |
172 | | - ); |
173 | | - |
174 | | - return new InstanceDescriptor(ci, new object[]{value}); |
175 | | - } |
176 | | - |
177 | | - // Pass unhandled cases to base class (which will throw exceptions for null value or destinationType.) |
178 | | - return base.ConvertTo(context, culture, value, destinationType); |
179 | | - } |
180 | | - |
181 | | - //--------------------------------- |
182 | | - // Private methods |
183 | | - //--------------------------------- |
184 | | - /// <summary> |
185 | | - /// Match the input against a predefined pattern from certain index onwards |
186 | | - /// </summary> |
187 | | - private static bool Match(string pattern, string input, int index) |
188 | | - { |
189 | | - int i = 0; |
190 | | - for (; |
191 | | - i < pattern.Length |
192 | | - && index + i < input.Length |
193 | | - && pattern[i] == Char.ToUpperInvariant(input[index + i]); |
194 | | - i++) ; |
195 | | - |
196 | | - return (i == pattern.Length); |
197 | | - } |
198 | | - |
199 | | - /// <summary> |
200 | | - /// Advance to the start of next name |
201 | | - /// </summary> |
202 | | - private static int AdvanceToNextNameStart(string input, int index) |
203 | | - { |
204 | | - // Two names must be seperated by a comma and optionally spaces |
205 | | - int separator = AdvanceToNextNonWhiteSpace(input, index); |
| 137 | + ConstructorInfo ci = typeof(TextDecorationCollection).GetConstructor(new Type[] { typeof(IEnumerable<TextDecoration>) }); |
206 | 138 |
|
207 | | - int nextNameStart; |
208 | | - if (separator >= input.Length) |
209 | | - { |
210 | | - // reach the end |
211 | | - nextNameStart = input.Length; |
212 | | - } |
213 | | - else |
214 | | - { |
215 | | - if (input[separator] == Separator) |
216 | | - { |
217 | | - nextNameStart = AdvanceToNextNonWhiteSpace(input, separator + 1); |
218 | | - if (nextNameStart >= input.Length) |
219 | | - { |
220 | | - // Error: Separator is at the end of the input |
221 | | - nextNameStart = -1; |
222 | | - } |
223 | | - } |
224 | | - else |
225 | | - { |
226 | | - // Error: There is a non-whitespace, non-separator character following |
227 | | - // the matched value |
228 | | - nextNameStart = -1; |
229 | | - } |
| 139 | + return new InstanceDescriptor(ci, new object[] { value }); |
230 | 140 | } |
231 | 141 |
|
232 | | - return nextNameStart; |
| 142 | + // Pass unhandled cases to base class (which will throw exceptions for null value or destinationType.) |
| 143 | + return base.ConvertTo(context, culture, value, destinationType); |
233 | 144 | } |
234 | 145 |
|
235 | 146 | /// <summary> |
236 | | - /// Advance to the next non-whitespace character |
| 147 | + /// Abstraction helper of matched decorations during conversion. |
237 | 148 | /// </summary> |
238 | | - private static int AdvanceToNextNonWhiteSpace(string input, int index) |
| 149 | + [Flags] |
| 150 | + private enum Decorations : byte |
239 | 151 | { |
240 | | - for (; index < input.Length && Char.IsWhiteSpace(input[index]); index++) ; |
241 | | - return (index > input.Length) ? input.Length : index; |
| 152 | + None = 0, |
| 153 | + OverlineMatch = 1 << 0, |
| 154 | + BaselineMatch = 1 << 1, |
| 155 | + UnderlineMatch = 1 << 2, |
| 156 | + StrikethroughMatch = 1 << 3, |
242 | 157 | } |
243 | | - |
244 | | - //--------------------------------- |
245 | | - // Private members |
246 | | - //--------------------------------- |
247 | | - |
248 | | - // |
249 | | - // Predefined valid names for TextDecorations |
250 | | - // Names should be normalized to be upper case |
251 | | - // |
252 | | - private const string None = "NONE"; |
253 | | - private const char Separator = ','; |
254 | | - |
255 | | - private static readonly string[] TextDecorationNames = new string[] { |
256 | | - "OVERLINE", |
257 | | - "BASELINE", |
258 | | - "UNDERLINE", |
259 | | - "STRIKETHROUGH" |
260 | | - }; |
261 | | - |
262 | | - // Predefined TextDecorationCollection values. It should match |
263 | | - // the TextDecorationNames array |
264 | | - private static readonly TextDecorationCollection[] PredefinedTextDecorations = |
265 | | - new TextDecorationCollection[] { |
266 | | - TextDecorations.OverLine, |
267 | | - TextDecorations.Baseline, |
268 | | - TextDecorations.Underline, |
269 | | - TextDecorations.Strikethrough |
270 | | - }; |
271 | | -} |
| 158 | + } |
272 | 159 | } |
0 commit comments