Разбор неверного XML или HTML с помощью XSLT в C #

0

Недавно мне пришлось написать класс, который обрабатывал бы предоставленный шаблон и возвращал результат. Я выбрал 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">, что означает, что я могу напрямую выбирать поля. К сожалению, у меня все еще было несколько проблем:

  1. Во-первых, шаблон, который я предоставлял, имел объявление HTML Doctype в верхней части страницы. Я начал получать ошибки вокруг "неожиданного объявления DTD", потому что DOCTYPE появлялся внутри узла xsl:template.
  2. Если пользователь предоставил какой-либо недопустимый XML (например, тег, который не был сам закрыт и не имел соответствующего закрывающего тега), тогда синтаксический анализатор будет генерировать исключение, даже если бы HTML работал в браузере. Это казалось неприемлемым, поскольку я хотел бы, чтобы мои пользователи поставляли идеально сформированные шаблоны XML/HTML, я не хочу переносить нагрузку на конечного пользователя, если это то, что я могу исправить.
  3. В HTML-шаблоне, который я тестировал, открытый HTML-тег был окружен условными комментариями IE, чтобы предоставить другой класс тегу в зависимости от версии IE. Например, <!--[if lt IE 7 ]><html class="ie ie6" lang="en"> <![endif]-->. Поскольку это проприетарный синтаксис IE, XML просто видит комментарий и не тег html. Закрывающий тег в конце документа поэтому не имеет соответствующего начального тега и генерирует исключение.

Когда я решал каждую проблему по одному, решение становилось все менее и менее приемлемым для меня, и я искал надежное решение, которое не заставило бы пользователя удовлетворять слабости моего метода шаблона.

Теги:
templates
xslt

1 ответ

0
Лучший ответ

В итоге я решил решить проблему, которая позволила бы недействительным 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 &lt; and &gt; 
/// 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;
}

Я объясню каждую строку в свою очередь:

  1. Первая линия заменяет открытие шевронов на% lt; маркер.
  2. Следующая строка заменяет закрывающие шевроны на% gt; маркер.
  3. Это ищет любые открытые теги XSL и превращает наши токены обратно в шевроны.
  4. (То же, что и 3, но для закрытия тегов XSL)
  5. Это ищет <xsl:attribute> и заменяет ближайший предшествующий токенированный тег соответствующим тегом XML. Тег xsl: attribute применяется к ближайшему узлу-предку, поэтому нам нужно преобразовать наш токенизированный тег в соответствующий XML для его работы.
  6. Подобно 5, это заменяет любой токенизированный закрывающий тег на родительском узле xsl: attribute с соответствующим тегом закрытия XML.

Дальнейшее объяснение 5 и 6

Учитывая следующий шаблон:

<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: атрибут, прежде чем пытаться заменить его родительские теги, например.

Удачи и не стесняйтесь предлагать какие-либо альтернативные предложения или улучшения.

  • 0
    Возможно, вы захотите взглянуть на пакет HtmlAgility. Регулярные выражения и xml, не очень хорошая идея, а производительность Xslt сама по себе отвратительна.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню