|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
namespace XliffTasks.Model
{
/// <summary>
/// A <see cref="TranslatableDocument"/> for files in CPS rule .xaml format
/// See https://msdn.microsoft.com/en-us/library/ekyft91f(v=vs.100).aspx
/// </summary>
internal sealed class XamlRuleDocument : TranslatableXmlDocument
{
private const string XliffTasksNs = "https://github.com/dotnet/xliff-tasks";
private const string LocalizedPropertiesAttributeName = "LocalizedProperties";
private static readonly char[] s_attrValueTrimChars = [ ':', ' ', '\t' ];
protected override IEnumerable<TranslatableNode> GetTranslatableNodes()
{
foreach (XElement? element in Document.Descendants())
{
// first, let's check if the element has a descendent by the name of {elementLocalName.Description/DisplayName}
var descendentDisplayName = element.Elements(XName.Get($"{element.Name.LocalName}.DisplayName", element.Name.NamespaceName)).FirstOrDefault();
var descendentDescription = element.Elements(XName.Get($"{element.Name.LocalName}.Description", element.Name.NamespaceName)).FirstOrDefault();
if (descendentDisplayName is not null)
{
yield return new TranslatableXmlElement(
id: GenerateIdForDisplayNameOrDescription(descendentDisplayName),
source: descendentDisplayName.Value,
note: GetComment(descendentDisplayName, XmlName(descendentDisplayName)),
element: descendentDisplayName
);
}
if (descendentDescription is not null)
{
yield return new TranslatableXmlElement(
id: GenerateIdForDisplayNameOrDescription(descendentDescription),
source: descendentDescription.Value,
note: GetComment(descendentDescription, XmlName(descendentDescription)),
element: descendentDescription
);
}
var localizableProperties = element.Attribute(XName.Get(LocalizedPropertiesAttributeName, XliffTasksNs))?.Value?.Split(';');
if (localizableProperties is not null)
{
// we could have any number of descendent localizable properties
foreach (var localizableProperty in localizableProperties)
{
if (element.Elements(XName.Get($"{element.Name.LocalName}.{localizableProperty}", element.Name.NamespaceName)).FirstOrDefault() is { } descendentValue)
{
yield return new TranslatableXmlElement(
id: GenerateIdForPropertyMetadata(descendentValue),
source: descendentValue.Value,
note: GetComment(descendentValue, localizableProperty),
element: descendentValue);
}
}
}
foreach (XAttribute? attribute in element.Attributes())
{
if ((descendentDisplayName is null && XmlName(attribute) == "DisplayName")
|| (descendentDescription is null && XmlName(attribute) == "Description"))
{
yield return new TranslatableXmlAttribute(
id: GenerateIdForDisplayNameOrDescription(attribute),
source: attribute.Value,
note: GetComment(element, XmlName(attribute)),
attribute: attribute);
}
else if (AttributedName(element) == "SearchTerms" && (XmlName(attribute) == "Value" || element.Elements(XName.Get($"{element.Name.LocalName}.Value", element.Name.NamespaceName)).FirstOrDefault() is { }))
{
if (XmlName(attribute) == "Value")
{
yield return new TranslatableXmlAttribute(
id: GenerateIdForPropertyMetadata(element),
source: attribute.Value,
note: GetComment(element, XmlName(attribute)),
attribute: attribute);
}
// else if we have a descendent in the form of {elementLocalName}.Value, we should translate that descendent
else if (element.Elements(XName.Get($"{element.Name.LocalName}.Value", element.Name.NamespaceName)).FirstOrDefault() is { } descendentValue)
{
yield return new TranslatableXmlElement(
id: GenerateIdForPropertyMetadata(element),
source: descendentValue.Value,
note: GetComment(descendentValue, XmlName(attribute)),
element: descendentValue);
}
}
else
{
if (localizableProperties is null)
{
continue;
}
// if the property value is directly specified as an attribute
if (localizableProperties.Contains(attribute.Name.LocalName))
{
yield return new TranslatableXmlAttribute(
id: GenerateIdForPropertyMetadata(element, attribute),
source: attribute.Value,
note: GetComment(element, XmlName(attribute)),
attribute: attribute);
}
}
}
}
}
private static string GenerateIdForDisplayNameOrDescription(XObject xObject)
{
var parent = xObject.Parent;
if (parent is null)
{
throw new ArgumentException("Attribute must have a parent element", nameof(xObject));
}
if (XmlName(parent) == "EnumValue")
{
var grandparent = parent.Parent;
if (grandparent is null)
{
throw new ArgumentException("Attribute must have a grandparent element", nameof(xObject));
}
return $"{XmlName(parent)}|{AttributedName(grandparent)}.{AttributedName(parent)}|{XmlName(xObject)}";
}
return $"{XmlName(parent)}|{AttributedName(parent)}|{XmlName(xObject)}";
}
private static string GenerateIdForPropertyMetadata(XElement element, XAttribute? attribute = null)
{
var ancestorWithNameAttributeCandidate = element.Parent?.Parent; // start at grandparent
var idBuilder = new StringBuilder();
// if has no grandparent, we'll try parent
if (ancestorWithNameAttributeCandidate is null)
{
if (element.Parent is not null)
{
idBuilder.Append(element.Parent.Attribute("Name") is null ? XmlName(element.Parent) : $"{XmlName(element.Parent)}|{AttributedName(element.Parent)}");
idBuilder.Append("|Metadata|");
}
idBuilder.Append(XmlName(element));
if (element.Attribute("Name") is null)
{
idBuilder.Append($"|{AttributedName(element)}");
}
if (attribute is not null)
{
idBuilder.Append($"|{XmlName(attribute)}");
}
return idBuilder.ToString();
}
// while the current ancestor has a parent and does not have a name, append its XmlName to the id and go up a level
while (ancestorWithNameAttributeCandidate?.Attribute("Name") is null && ancestorWithNameAttributeCandidate?.Parent is not null) {
{
idBuilder.Insert(0, $"{XmlName(ancestorWithNameAttributeCandidate)}|");
ancestorWithNameAttributeCandidate = ancestorWithNameAttributeCandidate.Parent;
}}
idBuilder.Insert(0, $"{XmlName(ancestorWithNameAttributeCandidate!)}|{AttributedName(ancestorWithNameAttributeCandidate!)}|");
idBuilder.Append($"Metadata|");
idBuilder.Append(element.Attribute("Name") is not null ? AttributedName(element) : XmlName(element));
if (attribute is not null)
{
idBuilder.Append($"|{attribute.Name.LocalName}");
}
return idBuilder.ToString();
}
private static string? GetComment(XElement element, string attributeName)
{
foreach (XComment comment in element.Nodes().OfType<XComment>())
{
foreach (string? line in comment.Value.Split(new[] { '\n' }, System.StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()))
{
if (line.StartsWith(attributeName))
{
return line.Substring(attributeName.Length).Trim(s_attrValueTrimChars);
}
}
}
return null;
}
private static string XmlName(XObject container) => container is XElement element ? XmlName(element) : XmlName((XAttribute)container);
private static string XmlName(XElement element)
{
var localName = element.Name.LocalName;
// if we have a descendent element, we should only take the last part of the name after the dot
return localName.Contains('.') ? localName.Split('.').Last() : localName;
}
private static string XmlName(XAttribute attribute) => attribute.Name.LocalName;
private static string? AttributedName(XElement element) => element.Attribute("Name")?.Value;
}
}
|