Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Linq;
using System.Xml;
using XPathTests.Common;
using Xunit;
Expand Down Expand Up @@ -1064,6 +1065,77 @@ public static void StringFunctionsTest2454(Utils.NavigatorKind kind)
Utils.XPathStringTest(kind, xml, testExpression, expected, startingNodePath: startingNodePath);
}

/// <summary>
/// Verify result.
/// translate("abc", "xyz", "XYZ") = "abc"
/// </summary>
[Theory]
[InlineData(Utils.NavigatorKind.XmlDocument)]
[InlineData(Utils.NavigatorKind.XPathDocument)]
[InlineData(Utils.NavigatorKind.XDocument)]
public static void StringFunctionsTestTranslateNoMatch(Utils.NavigatorKind kind)
{
var xml = "dummy.xml";
var testExpression = @"translate(""abc"", ""xyz"", ""XYZ"")";
var expected = "abc";

Utils.XPathStringTest(kind, xml, testExpression, expected);
}

/// <summary>
/// Verify result for strings longer than the stack buffer used internally.
/// translate(long string, "ab-", "AB") = long string with 'a' and 'b' mapped and '-' removed
/// </summary>
[Theory]
[InlineData(Utils.NavigatorKind.XmlDocument)]
[InlineData(Utils.NavigatorKind.XPathDocument)]
[InlineData(Utils.NavigatorKind.XDocument)]
public static void StringFunctionsTestTranslateLongString(Utils.NavigatorKind kind)
{
var xml = "dummy.xml";
var value = string.Concat(Enumerable.Repeat("xab-", 200));
var testExpression = @"translate(""" + value + @""", ""ab-"", ""AB"")";
var expected = string.Concat(Enumerable.Repeat("xAB", 200));

Utils.XPathStringTest(kind, xml, testExpression, expected);
}

/// <summary>
/// Verify result for strings longer than the stack buffer used internally.
/// normalize-space(long string) = long string with whitespace normalized
/// </summary>
[Theory]
[InlineData(Utils.NavigatorKind.XmlDocument)]
[InlineData(Utils.NavigatorKind.XPathDocument)]
[InlineData(Utils.NavigatorKind.XDocument)]
public static void StringFunctionsTestNormalizeSpaceLongString(Utils.NavigatorKind kind)
{
var xml = "dummy.xml";
var value = " " + string.Concat(Enumerable.Repeat("ab \t\n\r", 200)) + " ";
var testExpression = @"normalize-space(""" + value + @""")";
var expected = string.Join(" ", Enumerable.Repeat("ab", 200));

Utils.XPathStringTest(kind, xml, testExpression, expected);
}

/// <summary>
/// Verify result for already normalized strings longer than the stack buffer used internally.
/// normalize-space(long normalized string) = long normalized string
/// </summary>
[Theory]
[InlineData(Utils.NavigatorKind.XmlDocument)]
[InlineData(Utils.NavigatorKind.XPathDocument)]
[InlineData(Utils.NavigatorKind.XDocument)]
public static void StringFunctionsTestNormalizeSpaceLongNormalizedString(Utils.NavigatorKind kind)
{
var xml = "dummy.xml";
var value = string.Join(" ", Enumerable.Repeat("ab", 200));
var testExpression = @"normalize-space(""" + value + @""")";
var expected = value;

Utils.XPathStringTest(kind, xml, testExpression, expected);
}

/// <summary>
/// string(NaN)
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;
using System.Xml.Xsl.Runtime;

namespace MS.Internal.Xml.XPath
{
Expand Down Expand Up @@ -225,31 +226,7 @@ private string Normalize(XPathNodeIterator nodeIterator)
Debug.Assert(nodeIterator!.Current != null);
value = nodeIterator.Current.Value;
}
int modifyPos = -1;
char[] chars = value.ToCharArray();
bool firstSpace = false; // Start false to trim the beginning

for (int comparePos = 0; comparePos < chars.Length; comparePos++)
{
if (!XmlCharType.IsWhiteSpace(chars[comparePos]))
{
firstSpace = true;
modifyPos++;
chars[modifyPos] = chars[comparePos];
}
else if (firstSpace)
{
firstSpace = false;
modifyPos++;
chars[modifyPos] = ' ';
}
}

// Trim end
if (modifyPos > -1 && chars[modifyPos] == ' ')
modifyPos--;

return new string(chars, 0, modifyPos + 1);
return XsltFunctions.NormalizeSpace(value);
}

private string Translate(XPathNodeIterator nodeIterator)
Expand All @@ -258,28 +235,7 @@ private string Translate(XPathNodeIterator nodeIterator)
string value = _argList[0].Evaluate(nodeIterator).ToString()!;
string mapFrom = _argList[1].Evaluate(nodeIterator).ToString()!;
string mapTo = _argList[2].Evaluate(nodeIterator).ToString()!;
int modifyPos = -1;
char[] chars = value.ToCharArray();

for (int comparePos = 0; comparePos < chars.Length; comparePos++)
{
int index = mapFrom.IndexOf(chars[comparePos]);
if (index != -1)
{
if (index < mapTo.Length)
{
modifyPos++;
chars[modifyPos] = mapTo[index];
}
}
else
{
modifyPos++;
chars[modifyPos] = chars[comparePos];
}
}

return new string(chars, 0, modifyPos + 1);
return XsltFunctions.Translate(value, mapFrom, mapTo);
}

public override XPathNodeIterator Clone() { return new StringFunctions(this); }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
Expand All @@ -21,6 +22,8 @@ namespace System.Xml.Xsl.Runtime
[EditorBrowsable(EditorBrowsableState.Never)]
public static class XsltFunctions
{
private const int StackallocThreshold = 256;

//------------------------------------------------
// Xslt/XPath functions
//------------------------------------------------
Expand Down Expand Up @@ -100,95 +103,79 @@ public static string Substring(string value, double startIndex, double length)

public static string NormalizeSpace(string value)
{
StringBuilder? sb = null;
int idx, idxStart = 0, idxSpace = 0;
char[]? rented = null;
Span<char> chars = value.Length <= StackallocThreshold ?
stackalloc char[StackallocThreshold] :
(rented = ArrayPool<char>.Shared.Rent(value.Length));

for (idx = 0; idx < value.Length; idx++)
int length = 0;
bool firstSpace = false; // Start false to trim the beginning
foreach (char c in value)
{
if (XmlCharType.IsWhiteSpace(value[idx]))
if (!XmlCharType.IsWhiteSpace(c))
{
firstSpace = true;
chars[length++] = c;
}
else if (firstSpace)
{
if (idx == idxStart)
{
// Previous character was a whitespace character, so discard this character
idxStart++;
}
else if (value[idx] != ' ' || idxSpace == idx)
{
// Space was previous character or this is a non-space character
if (sb == null)
{
sb = new StringBuilder(value.Length);
}
else
{
sb.Append(' ');
}

// Copy non-space characters into string builder
sb.Append(value, idxStart, idxSpace == idx ? idx - idxStart - 1 : idx - idxStart);

idxStart = idx + 1;
}
else
{
// Single whitespace character doesn't cause normalization, but mark its position
idxSpace = idx + 1;
}
firstSpace = false;
chars[length++] = ' ';
}
}

if (sb == null)
// Trim end
if (length > 0 && chars[length - 1] == ' ')
{
// Check for string that is entirely composed of whitespace
if (idxStart == idx) return string.Empty;
length--;
}

// If string does not end with a space, then it must already be normalized
if (idxStart == 0 && idxSpace != idx) return value;
// If the string is already normalized, return it rather than allocating a copy
ReadOnlySpan<char> normalized = chars.Slice(0, length);
string result = normalized.SequenceEqual(value) ? value : normalized.ToString();

sb = new StringBuilder(value.Length);
}
else if (idx != idxStart)
if (rented != null)
{
sb.Append(' ');
ArrayPool<char>.Shared.Return(rented);
}

// Copy non-space characters into string builder
if (idxSpace == idx)
sb.Append(value, idxStart, idx - idxStart - 1);
else
sb.Append(value, idxStart, idx - idxStart);

return sb.ToString();
return result;
}

public static string Translate(string arg, string mapString, string transString)
{
if (mapString.Length == 0)
// Skip the leading characters that are not mapped; if all of them are, the argument is returned unchanged
int i = 0;
while (i < arg.Length && mapString.IndexOf(arg[i]) < 0)
{
i++;
}
if (i == arg.Length)
{
return arg;
}

StringBuilder sb = new StringBuilder(arg.Length);

for (int i = 0; i < arg.Length; i++)
char[] chars = arg.ToCharArray();
int length = i;
for (; i < chars.Length; i++)
{
int index = mapString.IndexOf(arg[i]);
int index = mapString.IndexOf(chars[i]);
if (index < 0)
{
// Keep the character
sb.Append(arg[i]);
chars[length++] = chars[i];
}
else if (index < transString.Length)
{
// Replace the character
sb.Append(transString[index]);
chars[length++] = transString[index];
}
else
{
// Remove the character
}
}
return sb.ToString();

return new string(chars, 0, length);
}

public static bool Lang(string value, XPathNavigator context)
Expand Down