在 CMS、部落格摘要、搜尋結果等場景,需將含標籤的 HTML/UBB 字串截斷至指定長度。直接 Substring 會導致標籤未閉合、版面崩潰。本文實作基於堆疊追蹤標籤狀態的安全截斷演算法,支援自訂標籤字元、指定必閉合標籤、半截標籤保留/移除策略。
<)進入標籤解析狀態<div>)或關閉標籤(</div>)public static class HtmlTruncator
{
private const char EndSlash = '/';
///
/// 安全截斷 HTML/UBB 字串
///
public static string SafeTrim(
string input,
int length,
bool trimHalfTag,
char tagStartChar,
char tagEndChar,
string[] mustCloseTags)
{
if (length <= 0) throw new ArgumentException("length 必須為正數");
if (mustCloseTags == null) throw new ArgumentNullException(nameof(mustCloseTags));
if (string.IsNullOrEmpty(input) || input.Length <= length) return input;
var result = new StringBuilder(length + 50); // 預留補全標籤空間
var tags = new Stack();
for (int i = 0; i < length; i++)
{
char c = input[i];
if (c == tagStartChar)
{
result.Append(c);
var tag = new StringBuilder();
int tagIndex = i + 1;
bool isTagEnd = false;
bool isTagNameEnd = false;
bool hasMarked = false;
while (tagIndex < input.Length)
{
char tagC = input[tagIndex];
result.Append(tagC);
tagIndex++;
if (tag.Length == 0 && tagC == EndSlash)
{
isTagEnd = true;
continue;
}
if (!isTagNameEnd)
{
if (char.IsLetterOrDigit(tagC))
tag.Append(tagC);
else
isTagNameEnd = true;
}
if (tag.Length > 0 && isTagNameEnd && !hasMarked)
{
if (isTagEnd)
tags.Pop();
else
tags.Push(tag.ToString());
hasMarked = true;
}
if (isTagNameEnd && tagC == tagEndChar)
{
i = tagIndex - 1;
break;
}
}
}
else
{
result.Append(c);
}
}
// 處理未閉合標籤
while (tags.Count > 0)
{
string tag = tags.Pop();
bool isMustClose = mustCloseTags.Any(t =>
string.Equals(t, tag, StringComparison.OrdinalIgnoreCase));
if (isMustClose)
{
if (trimHalfTag)
{
// 移除半截標籤
int lastIdx = result.ToString().LastIndexOf(
tagStartChar + tag, StringComparison.OrdinalIgnoreCase);
if (lastIdx >= 0)
result.Length = lastIdx;
}
else
{
// 補全關閉標籤
result.Append(tagStartChar).Append(EndSlash).Append(tag).Append(tagEndChar);
}
}
}
return result.ToString();
}
}
var html = "<div><p>這是一段<strong>重要</strong>內容</p><span>尾巴</span></div>";
// 截斷 20 字,補全標籤,強制閉合 div、p、strong
var trimmed = HtmlTruncator.SafeTrim(
html, 20,
trimHalfTag: false,
tagStartChar: '<',
tagEndChar: '>',
mustCloseTags: new[] { "div", "p", "strong", "span" });
// 結果: "<div><p>這是一段<strong>重</strong></p></div>"
var ubb = "[b]粗體[/b][color=red]紅字[/color]";
var trimmed = HtmlTruncator.SafeTrim(
ubb, 10, false, '[', ']', new[] { "b", "color" });
// 結果: "[b]粗體[/b][color=red]紅[/color]"
HtmlAgilityPack 整合選項:複雜 HTML 結構建議改用 DOM 解析void 標籤(<br/>, <img>)自動識別,不納入堆疊| 場景 | 建議策略 |
|---|---|
| 文章摘要列表 | 補全標籤,保留版面結構 |
| 搜尋關鍵字高亮片段 | 移除半截標籤,避免殘留樣式干擾 |
| UBB 論壇回覆預覽 | 自訂 [ ] 標籤字元,強制閉合常用標籤 |
| 郵件內容純文字預覽 | trimHalfTag=true,完全移除所有標籤 |
此演算法已在多個產品專案驗證穩定,歡迎參考改良。若有更優雅的實作(如正則預編譯、狀態機優化),歡迎分享交流。