Усечение содержимого HTML в конце текстовых блоков (элементов блока)

В основном, когда мы сокращаем/усекаем текстовый контент, мы обычно просто обрезаем его по конкретному индексу символов. Это уже слишком сложно в HTML, но я хочу урезать свой HTML-контент (сгенерированный с помощью редактируемого контента div) с использованием различных мер:

  • Я бы определил индекс символа N, который будет использоваться как начальная точка усечения limit
  • Алгоритм проверяет, имеет ли контент не менее N символов (только текст, не считая теги); если нет, он просто вернет весь контент
  • Затем он будет проверять от N-X до N+X позицию символа (только текст) и искать концы узлов блока; X является предопределенным значением offset и вероятностью примерно от N/5 до N/4;
  • Если несколько узлов блока заканчиваются в этом диапазоне, алгоритм выберет тот, который будет ближе всего к пределу индекса N
  • Если в этом диапазоне не заканчивается блок node, он найдет ближайшую границу слова в пределах того же диапазона и выберите индекс, ближайший к N, и усечет в этой позиции.
  • Возвращает усеченное содержимое с допустимым HTML (все теги закрыты в конце)

Мое содержимое, редактируемое для контента, может состоять из абзацев (с разрывами строк), предварительно отформатированных блоков кода, кавычек блоков, упорядоченных и неупорядоченных списков, заголовков, полужирных и курсивых (которые являются встроенными узлами и не должны учитываться в процессе усечения) Конечная реализация, конечно, определит, какие элементы конкретно являются возможными кандидатами усечения. Заголовки, даже если они являются блочными элементами HTML, не будут считаться точками усечения, поскольку нам не нужны овдовевшие заголовки. Пункты, список отдельных элементов, целые упорядоченные и неупорядоченные списки, блок-кавычки, предварительно отформатированные блоки, элементы пустот и т.д. Являются хорошими. Заголовки и все встроенные элементы блока не являются.

Пример

Возьмем этот самый вопрос stackoverflow как пример содержимого HTML, который я хотел бы усечь. Пусть установлен предел усечения 1000 со смещением символов 250 (1/4).

Этот DotNetFiddle показывает текст этого вопроса, а также добавляет в него ограничивающие маркеры (|MIN|, который представляет символ 750, |LIMIT|, представляющий символ 1000 и |MAX|, который представляет символ 1250).

Как видно из примера , ближайшая граница усечения между двумя блочными узлами для символа 1000 находится между </OL> и P (My content-editable generated...). Это означает, что мой HTML-код должен быть усечен прямо между этими двумя тегами, что приведет к содержанию текста с текстом чуть меньше 1000 символов, но содержало усеченный контент значимым, потому что он не просто усекал бы где-нибудь посередине некоторого текстового прохода.

Надеюсь, это объяснит, как нужно работать над этим алгоритмом.

Проблема

Первая проблема, которую я вижу здесь, заключается в том, что я имею дело с вложенной структурой, такой как HTML. Я также должен обнаруживать разные элементы (только элементы блока и не встроенные). И последнее, но не в последнюю очередь, мне придется только подсчитать некоторые символы в моей строке и игнорировать те, которые относятся к тегам.

Возможные решения

  • Я мог бы вручную разобрать свой контент, создав некоторое дерево объектов, представляющее узлы контента и их иерархию
  • Я мог бы преобразовать HTML в нечто более удобное для управления, например markdown, а затем просто искать ближайшую новую строку для моего предоставленного индекса N и конвертировать обратно в HTML
  • Используйте что-то вроде HTML Agility Pack и замените мой синтаксический анализ # 1, а затем каким-то образом используйте XPath для извлечения узлов блока и усечения содержимого

Вторые мысли

  • Я уверен, что смогу сделать это, заняв # 1, но мне кажется, что я изобретаю колесо.
  • Я не думаю, что есть библиотека С# для # 2, поэтому я должен делать HTML для Markdown вручную или запускать i.e.pandoc как внешний процесс.
  • Я мог бы использовать HAP, поскольку он отлично справляется с управлением HTML, но я не уверен, будет ли мое усечение достаточно простым, используя его. Я боюсь, что основная часть обработки будет по-прежнему находиться вне HAP в моем пользовательском коде.

Как следует подходить к таким алгоритмам усечения? Моя голова кажется слишком усталой, чтобы прийти к консенсусу (или решению).

Ответ 1

Вот пример кода, который может обрезать внутренний текст. Он использует рекурсивную способность свойства InnerText и CloneNode.

    public static HtmlNode TruncateInnerText(HtmlNode node, int length)
    {
        if (node == null)
            throw new ArgumentNullException("node");

        // nothing to do?
        if (node.InnerText.Length < length)
            return node;

        HtmlNode clone = node.CloneNode(false);
        TruncateInnerText(node, clone, clone, length);
        return clone;
    }

    private static void TruncateInnerText(HtmlNode source, HtmlNode root, HtmlNode current, int length)
    {
        HtmlNode childClone;
        foreach (HtmlNode child in source.ChildNodes)
        {
            // is expected size is ok?
            int expectedSize = child.InnerText.Length + root.InnerText.Length;
            if (expectedSize <= length)
            {
                // yes, just clone the whole hierarchy
                childClone = child.CloneNode(true);
                current.ChildNodes.Add(childClone);
                continue;
            }

            // is it a text node? then crop it
            HtmlTextNode text = child as HtmlTextNode;
            if (text != null)
            {
                int remove = expectedSize - length;
                childClone = root.OwnerDocument.CreateTextNode(text.InnerText.Substring(0, text.InnerText.Length - remove));
                current.ChildNodes.Add(childClone);
                return;
            }

            // it not a text node, shallow clone and dive in
            childClone = child.CloneNode(false);
            current.ChildNodes.Add(childClone);
            TruncateInnerText(child, root, childClone, length);
        }
    }

И примерное консольное приложение С#, которое рассмотрит этот вопрос в качестве примера и усекает его до 500 символов.

  class Program
  {
      static void Main(string[] args)
      {
          var web = new HtmlWeb();
          var doc = web.Load("http://stackoverflow.com/questions/30926684/truncating-html-content-at-the-end-of-text-blocks-block-elements");
          var post = doc.DocumentNode.SelectSingleNode("//td[@class='postcell']//div[@class='post-text']");
          var truncated = TruncateInnerText(post, 500);
          Console.WriteLine(truncated.OuterHtml);
          Console.WriteLine("Size: " + truncated.InnerText.Length);
      }
  }

При запуске он должен отображать это:

<div class="post-text" itemprop="text">

<p>Mainly when we shorten/truncate textual content we usually just truncate it at specific character index. That already complicated in HTML anyway, but I want to truncate my HTML content (generated using content-editable <code>div</code>) using different measures:</p>

<ol>
<li>I would define character index <code>N</code> that will serve as truncating startpoint <em>limit</em></li>
<li>Algorithm will check whether content is at least <code>N</code> characters long (text only; not counting tags); if it not it will just return the whole content</li>
<li>It would then</li></ol></div>
Size: 500

Примечание. Я не усекался на границе слова, только на границе символа, и нет, это совсем не соответствует рекомендациям в моем комментарии: -)

Ответ 2

   private void RemoveEmpty(HtmlNode node){
       var parent = node.Parent;
       node.Remove();
       if(parent==null)
           return;
       // remove parent if it is empty
       if(!parent.DescendantNodes.Any()){
           RemoveEmpty(parent);
       }
   }



private void Truncate(DocumentNode root, int maxLimit){

    var n = 0;
    HtmlTextNode lastNode = null;

    foreach(var node in root.DescendantNodes
         .OfType<HtmlTextNode>().ToArray()){
       var length = node.Text.Length;

       n+= length;
       if(n + length >= maxLimit){
            RemoveEmpty(node);
       }

    }
}

// you are left with only nodes that add up to your max limit characters.

Ответ 3

Я буду запускать все дерево DOM и продолжать подсчитывать количество отображаемых символов текста. Всякий раз, когда я нажимаю предел (N), я стираю лишние символы этого текста node и оттуда я просто удалю все текстовые узлы.

Я считаю, что это безопасный способ сохранить всю структуру HTML + CSS, сохраняя только N символов.