Предлагаемое решение
Человек, это проклятие мое! Я, очевидно, не могу уйти от проблемы, не потратив на это необоснованно времени.
Я думал об этом. Я думал о HTML Tidy, и, возможно, он сработает, но мне было трудно обернуть его вокруг.
Итак, я написал собственное решение.
Я проверил это на вашем входе и на другом входе, который я сам собрал. Кажется, работает довольно хорошо. Конечно, в нем есть дыры, но это может дать вам отправную точку.
Во всяком случае, мой подход был такой:
- Инкапсулируйте понятие отдельного слова в документе HTML, используя класс, который включает информацию о положении этого слова в иерархии документа HTML, вплоть до заданной "вершины". Это я реализовал в
HtmlWord
классе ниже.
- Создайте класс, способный писать одну строку, состоящую из этих HTML-слов выше, так, чтобы теги start-element и end-element добавлялись в соответствующих местах. Это я реализовал в
HtmlLine
классе ниже.
- Напишите несколько методов расширения, чтобы сделать эти классы доступными сразу и интуитивно прямо из объекта
HtmlAgilityPack.HtmlNode
. Это я реализовал в HtmlHelper
классе ниже.
Я с ума сошел за все это? Вероятно, да. Но, вы знаете, если вы не можете найти какой-либо другой способ, вы можете попробовать это.
Вот как это работает с вашим примером ввода:
var document = new HtmlDocument();
document.LoadHtml("<p><strong>Lorem ipsum dolor sit amet, <em>consectetur adipiscing</em></strong> elit.</p>");
var nodeToSplit = document.DocumentNode.SelectSingleNode("p");
var lines = nodeToSplit.SplitIntoLines(3);
foreach (var line in lines)
Console.WriteLine(line.ToString());
Выход:
<p><strong>Lorem ipsum dolor </strong></p>
<p><strong>sit amet, <em>consectetur </em></strong></p>
<p><strong><em>adipiscing </em></strong>elit. </p>
А теперь код:
HtmlWord класс
using System;
using System.Collections.Generic;
using System.Linq;
using HtmlAgilityPack;
public class HtmlWord {
public string Text { get; private set; }
public HtmlNode[] NodeStack { get; private set; }
// convenience property to display list of ancestors cleanly
// (for ease of debugging)
public string NodeList {
get { return string.Join(", ", NodeStack.Select(n => n.Name).ToArray()); }
}
internal HtmlWord(string text, HtmlNode node, HtmlNode top) {
Text = text;
NodeStack = GetNodeStack(node, top);
}
private static HtmlNode[] GetNodeStack(HtmlNode node, HtmlNode top) {
var nodes = new Stack<HtmlNode>();
while (node != null && !node.Equals(top)) {
nodes.Push(node);
node = node.ParentNode;
};
return nodes.ToArray();
}
}
HtmlLine класс
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml;
using HtmlAgilityPack;
[Flags()]
public enum NodeChange {
None = 0,
Dropped = 1,
Added = 2
}
public class HtmlLine {
private List<HtmlWord> _words;
public IList<HtmlWord> Words {
get { return _words.AsReadOnly(); }
}
public int WordCount {
get { return _words.Count; }
}
public HtmlLine(IEnumerable<HtmlWord> words) {
_words = new List<HtmlWord>(words);
}
private static NodeChange CompareNodeStacks(HtmlWord x, HtmlWord y, out HtmlNode[] droppedNodes, out HtmlNode[] addedNodes) {
var droppedList = new List<HtmlNode>();
var addedList = new List<HtmlNode>();
// traverse x's NodeStack backwards to see which nodes
// do not include y (and are therefore "finished")
foreach (var node in x.NodeStack.Reverse()) {
if (!Array.Exists(y.NodeStack, n => n.Equals(node)))
droppedList.Add(node);
}
// traverse y's NodeStack forwards to see which nodes
// do not include x (and are therefore "new")
foreach (var node in y.NodeStack) {
if (!Array.Exists(x.NodeStack, n => n.Equals(node)))
addedList.Add(node);
}
droppedNodes = droppedList.ToArray();
addedNodes = addedList.ToArray();
NodeChange change = NodeChange.None;
if (droppedNodes.Length > 0)
change &= NodeChange.Dropped;
if (addedNodes.Length > 0)
change &= NodeChange.Added;
// could maybe use this in some later revision?
// not worth the effort right now...
return change;
}
public override string ToString() {
if (WordCount < 1)
return string.Empty;
var lineBuilder = new StringBuilder();
using (var lineWriter = new StringWriter(lineBuilder))
using (var xmlWriter = new XmlTextWriter(lineWriter)) {
var firstWord = _words[0];
foreach (var node in firstWord.NodeStack) {
xmlWriter.WriteStartElement(node.Name);
foreach (var attr in node.Attributes)
xmlWriter.WriteAttributeString(attr.Name, attr.Value);
}
xmlWriter.WriteString(firstWord.Text + " ");
for (int i = 1; i < WordCount; ++i) {
var previousWord = _words[i - 1];
var word = _words[i];
HtmlNode[] droppedNodes;
HtmlNode[] addedNodes;
CompareNodeStacks(
previousWord,
word,
out droppedNodes,
out addedNodes
);
foreach (var dropped in droppedNodes)
xmlWriter.WriteEndElement();
foreach (var added in addedNodes) {
xmlWriter.WriteStartElement(added.Name);
foreach (var attr in added.Attributes)
xmlWriter.WriteAttributeString(attr.Name, attr.Value);
}
xmlWriter.WriteString(word.Text + " ");
if (i == _words.Count - 1) {
foreach (var node in word.NodeStack)
xmlWriter.WriteEndElement();
}
}
}
return lineBuilder.ToString();
}
}
Статический класс HtmlHelper
using System;
using System.Collections.Generic;
using System.Linq;
using HtmlAgilityPack;
public static class HtmlHelper {
public static IList<HtmlLine> SplitIntoLines(this HtmlNode node, int wordsPerLine) {
var lines = new List<HtmlLine>();
var words = node.GetWords(node.ParentNode);
for (int i = 0; i < words.Count; i += wordsPerLine) {
lines.Add(new HtmlLine(words.Skip(i).Take(wordsPerLine)));
}
return lines.AsReadOnly();
}
public static IList<HtmlWord> GetWords(this HtmlNode node, HtmlNode top) {
var words = new List<HtmlWord>();
if (node.HasChildNodes) {
foreach (var child in node.ChildNodes)
words.AddRange(child.GetWords(top));
} else {
var textNode = node as HtmlTextNode;
if (textNode != null && !string.IsNullOrEmpty(textNode.Text)) {
string[] singleWords = textNode.Text.Split(
new string[] {" "},
StringSplitOptions.RemoveEmptyEntries
);
words.AddRange(
singleWords
.Select(w => new HtmlWord(w, node.ParentNode, top)
)
);
}
}
return words.AsReadOnly();
}
}
Заключение
Просто повторюсь: это комплексное решение; Я уверен, что у него есть проблемы. Я представляю это только в качестве отправной точки для рассмотрения - опять же, если вы не можете получить желаемое поведение другими способами.