Недавно мне пришлось написать класс, который обрабатывал бы предоставленный шаблон и возвращал результат. Я выбрал XSLT в качестве языка шаблонов из-за широкого применения в отрасли. Однако проблема была в том, что у предоставленного шаблона было несколько ограничений, которые оказывали боль. Вот пример моего кода:
public string ProcessTemplate(string template, IEnumerable<Field> fields)
{
// Surround the supplied template with the required XML
template = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<xsl:stylesheet version=""1.0"" xmlns:xsl=""http://www.w3.org/1999/XSL/Transform"">
<xsl:output method=""html"" version=""2.0"" encoding=""UTF-8"" indent=""yes""/>
<xsl:template match=""/entity"">"
+ template
+ "</xsl:template></xsl:stylesheet>";
// Turn our fields into XML with an "entity" tag as the root node
var t = GetTemplateXml(fields);
// Create a stringreader to read our template into memory
var sr = new StringReader(t.ToString());
var xr = new XmlTextReader(sr);
// Now create a XmlWriter attached to a StringBuilder to contain the transformed result
var sb = new StringBuilder();
var xws = new XmlWriterSettings()
{
ConformanceLevel = ConformanceLevel.Fragment
};
var xw = XmlWriter.Create(sb, xws);
// Create the transform object and an XmlReader for our template
var xsl = new XslCompiledTransform();
var xslr = XmlReader.Create(new StringReader(template));
// Load our template into the transform object, transform it, and put the result into our XmlWriter (and therefore into our StringBuilder)
xsl.Load(xslr);
xsl.Transform(xr, xw);
var res = sb.ToString();
return res;
}
Пользователь предоставит несколько объектов Field
, которые должны быть действительными. XML должен был совместно использовать корневой узел. Я назвал этот корневой узел "сущностью", но не хотел, чтобы пользователям приходилось выбирать "сущность" каждый раз, когда они обращаются к полю. Поэтому я окружаю шаблон с помощью <xsl:template match="/entity">
, что означает, что я могу напрямую выбирать поля. К сожалению, у меня все еще было несколько проблем:
DOCTYPE
появлялся внутри узла xsl:template
.<!--[if lt IE 7 ]><html class="ie ie6" lang="en"> <![endif]-->
. Поскольку это проприетарный синтаксис IE, XML просто видит комментарий и не тег html. Закрывающий тег в конце документа поэтому не имеет соответствующего начального тега и генерирует исключение.Когда я решал каждую проблему по одному, решение становилось все менее и менее приемлемым для меня, и я искал надежное решение, которое не заставило бы пользователя удовлетворять слабости моего метода шаблона.
В итоге я решил решить проблему, которая позволила бы недействительным HTML и передать эту ответственность обратно в браузер, где пользователь ожидает, что он будет лгать. Некоторые могут не согласиться с этим подходом, рассуждая о том, что для шаблона лучше быстро и явно выйти из строя, чтобы его можно было исправить. Однако, как я уже сказал, я обслуживаю конечного пользователя, у которого есть определенные ожидания от написания HTML - один из них заключается в том, что страница будет отображаться, если они предоставляют плохо сформированный HTML. Даже если он неправильно отображается.
Я решил, что я смогу обработать все три проблемы, описанные выше, написав метод, который подготовит шаблон перед преобразованием, а затем метод, который очистит результат, прежде чем вернуть его пользователю. Вот они:
/// <summary>
/// This function replaces all chevrons with %lt; and %gt; Any xsl tags are left in place
/// so they can be processed, and the parent tags of xsl:attribute tags are also left
/// untouched in order that the attribute can be correctly assigned.
/// The tokens used are deliberately different from the standard tokens of < and >
/// because we will want to revert these tokens later without reverting the normal tokens.
/// </summary>
/// <param name="template"></param>
/// <returns></returns>
private string PrepareTemplate(string template)
{
template = Regex.Replace(template, "<", "%lt;");
template = Regex.Replace(template, ">", "%gt;");
template = Regex.Replace(template, "%lt;xsl:(.*?)%gt;", "<xsl:$1>");
template = Regex.Replace(template, "%lt;/xsl:(.*?)%gt;", "</xsl:$+>");
template = Regex.Replace(template, "%lt;(.[^%]*?)%gt;(.[^%]*?)<xsl:attribute(.*?)>", "<$1>$2<xsl:attribute$3>", RegexOptions.Singleline);
template = Regex.Replace(template, "</xsl:attribute>(.*?)%lt;/(.*?)%gt;", "</xsl:attribute>$1</$2>", RegexOptions.Singleline);
return template;
}
Я объясню каждую строку в свою очередь:
<xsl:attribute>
и заменяет ближайший предшествующий токенированный тег соответствующим тегом XML. Тег xsl: attribute применяется к ближайшему узлу-предку, поэтому нам нужно преобразовать наш токенизированный тег в соответствующий XML для его работы.Учитывая следующий шаблон:
<a>
<xsl:attribute name="href">
<xsl:value-of select="url" />
</xsl:attribute>
<xsl:value-of select="linkText" />
</a>
Мы закончим тем, что все не-XSL-узлы будут маркироваться следующим образом:
%lt;a%gt;
<xsl:attribute name="href">
<xsl:value-of select="url" />
</xsl:attribute>
<xsl:value-of select="linkText" />
%lt;/a%gt;
Поскольку метка больше не является допустимым XML - тег, синтаксический анализатор не будет прикрепить атрибут к нужному объекту. a
Хуже того, он будет генерировать исключение, когда он пытается подключить его к корневому узлу. Глядя на предыдущей и следующей метке позволяет заменить a
тег в то время оставляя остальную часть документа tokenised.
проблема
К сожалению, у меня есть одно ограничение, которое я заметил, что любой тег, не a
XSL в теге, приведет к замене неправильного тега. Возьмем следующий пример:
<a>
<xsl:attribute name="href">
<xsl:value-of select="url" />
</xsl:attribute>
<xsl:value-of select="linkText" />
<span> - Click here</span>
</a>
Регулярное выражение будет заменить закрытие span
тега вместо закрытия a
теге, так что мы в конечном итоге с этим:
<a>
<xsl:attribute name="href">
<xsl:value-of select="url" />
</xsl:attribute>
<xsl:value-of select="linkText" />
%lt;span%gt; - Click here</span>
%lt;/a%gt;
Очевидно, что это означает, что открытие тег и закрывающий a
span
тегов не совпадают, что вызывает исключение. То же самое верно, если мы поместим span перед тегом xsl:attribute
за исключением того, что вместо этого откроется span
открытия вместо открытия a
. Моя первая мысль заключалась в поиске закрывающего тега, который соответствовал открытому тегу, который мы нашли, или наоборот, но поскольку один и тот же тег может быть вложен, он будет включать подсчет количества открывающих и закрывающих тегов, чтобы убедиться, что они совпадают. Это может стать беспорядочным.
Решение
К счастью, это легко решить какой-то обычно недействительный XSL. Причиной для xsl:attribute
назначить атрибут XML-узлу - обычно помещать xml внутри значения атрибута является недопустимым и генерирует исключение, но поскольку мы сделали токенизацию нашего XML, мы можем сделать это безопасно. Поэтому, чтобы добавить атрибут, мы просто вставляем обычный атрибут xsl:value-of
в атрибут так:
<a href="<xsl:value-of select="logo" />">
<xsl:value-of select="linkText" />
<span> - Click here</span>
</a>
Это было бы PrepareTemplate
, прежде чем мы запустим PrepareTemplate
, но потом это выглядит так:
%lt;a href="<xsl:value-of select="logo" />"%gt;
<xsl:value-of select="linkText" />
%lt;span%gt; - Click here%lt;/span%gt;
%lt;/a%gt;
Это совершенно корректный XML и имеет дополнительное преимущество, обеспечивающее более элегантный подход (по моему мнению), более похожий на большинство языков шаблонов. xsl:attribute
все равно будет работать, но только в том случае, если в теге нет дочерних элементов (кроме тегов xsl), к которым применяется атрибут.
Когда мы закончим обработку XML, мы вызываем следующий метод:
private string RevertTemplate(string template)
{
template = Regex.Replace(template, "%lt;", "<");
template = Regex.Replace(template, "%gt;", ">");
return template;
}
Это превращает наши специальные маркеры chevron обратно в соответствующие теги, оставляя только теги <и>.
Чтобы подготовить наш шаблон, мы вызываем PrepareTemplate
следующим образом:
template = @"<?xml version=""1.0"" encoding=""UTF-8""?>
<xsl:stylesheet version=""1.0"" xmlns:xsl=""http://www.w3.org/1999/XSL/Transform"">
<xsl:output method=""html"" version=""2.0"" encoding=""UTF-8"" indent=""yes""/>
<xsl:template match=""/entity"">"
+ PrepareTemplate(template)
+ "</xsl:template></xsl:stylesheet>";
И после преобразования XML мы вернем шаблон так:
var res = RevertTemplate(sb.ToString());
return res;
Надеюсь, это поможет кому-то столкнуться с подобной дилеммой. Хотя это не идеальное решение, оно является работоспособным. Очевидно, что вы должны быть осторожны, пытаясь использовать это в ситуации высокой производительности, поскольку регулярные выражения могут замедлить работу вашей системы, если вы пытаетесь обработать тысячи шаблонов. Стоит сделать некоторые проверки, чтобы увидеть, присутствует ли тэг xsl: атрибут, прежде чем пытаться заменить его родительские теги, например.
Удачи и не стесняйтесь предлагать какие-либо альтернативные предложения или улучшения.